Skip to content

Commit 856ccd7

Browse files
authored
fix!: remove node-forge dependency from @libp2p/crypto (#2355)
TLDR: the bundle size has been reduced by about 1/3rd - parsing/creating PEM/pkix/pkcs1 files is now done by asn1.js - Streaming AES-CTR ciphers are now in [@libp2p/aes-ctr](https://github.com/libp2p/js-libp2p-aes-ctr) - RSA encryption/decryption and PEM import/export are now in [@libp2p/rsa](https://github.com/libp2p/js-libp2p-rsa) ## AES-CTR WebCrypto [doesn't support streaming ciphers](w3c/webcrypto#73). We have a node-forge-backed shim that allows using streaming AES-CTR in browsers but we don't use it anywhere, so this has been split out into it's own module as `@libp2p/aes-ctr`. ## RSA encrypt/decrypt This was added to `@libp2p/crypto` to [support webrtc-stardust](libp2p/js-libp2p-crypto#125 (comment)) but that effort didn't go anywhere and we don't use these methods anywhere else in the stack. For reasons lost to the mists of time, we chose to use a [padding algorithm](https://github.com/libp2p/js-libp2p-crypto/blob/3d0fd234deb73984ddf0f7c9959bbca92194926a/src/keys/rsa.ts#L59) that WebCrypto doesn't support so node-forge (or some other userland implemenation) will always be necessary in browsers, so these ops have been pulled out into `@libp2p/rsa` which people can use if they need it. This is now done by manipulating the asn1 structures directly. ## PEM/pkix/pkcs1 The previous PEM import/export is also ported to `@libp2p/crypto-rsa` because it seems to handle more weird edge cases introduced by OpenSSL. These could be handled in `@libp2p/crypto` eventually but for now it at least supports round-tripping it's own PEM files. Fixes #2086 BREAKING CHANGE: Legacy RSA operations are now in @libp2p/rsa, streaming AES-CTR ciphers are in @libp2p/aes-ctr
1 parent ddaa59a commit 856ccd7

24 files changed

Lines changed: 515 additions & 829 deletions

packages/crypto/package.json

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@
5151
"types": "./src/index.d.ts",
5252
"import": "./dist/src/index.js"
5353
},
54-
"./aes": {
55-
"types": "./dist/src/aes/index.d.ts",
56-
"import": "./dist/src/aes/index.js"
57-
},
5854
"./hmac": {
5955
"types": "./dist/src/hmac/index.d.ts",
6056
"import": "./dist/src/hmac/index.js"
@@ -69,10 +65,7 @@
6965
"parserOptions": {
7066
"project": true,
7167
"sourceType": "module"
72-
},
73-
"ignorePatterns": [
74-
"src/*.d.ts"
75-
]
68+
}
7669
},
7770
"scripts": {
7871
"clean": "aegir clean",
@@ -92,11 +85,11 @@
9285
"dependencies": {
9386
"@libp2p/interface": "^1.1.1",
9487
"@noble/curves": "^1.1.0",
95-
"@noble/hashes": "^1.3.1",
88+
"@noble/hashes": "^1.3.3",
89+
"asn1js": "^3.0.5",
9690
"multiformats": "^13.0.0",
97-
"node-forge": "^1.1.0",
9891
"protons-runtime": "^5.0.0",
99-
"uint8arraylist": "^2.4.3",
92+
"uint8arraylist": "^2.4.7",
10093
"uint8arrays": "^5.0.0"
10194
},
10295
"devDependencies": {
@@ -106,12 +99,12 @@
10699
"protons": "^7.3.0"
107100
},
108101
"browser": {
109-
"./dist/src/aes/ciphers.js": "./dist/src/aes/ciphers-browser.js",
110102
"./dist/src/ciphers/aes-gcm.js": "./dist/src/ciphers/aes-gcm.browser.js",
111103
"./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js",
112104
"./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js",
113105
"./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js",
114106
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js",
115-
"./dist/src/keys/secp256k1.js": "./dist/src/keys/secp256k1-browser.js"
107+
"./dist/src/keys/secp256k1.js": "./dist/src/keys/secp256k1-browser.js",
108+
"./dist/src/webcrypto.js": "./dist/src/webcrypto-browser.js"
116109
}
117110
}

packages/crypto/src/aes/cipher-mode.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/crypto/src/aes/ciphers-browser.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

packages/crypto/src/aes/ciphers.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/crypto/src/aes/index.ts

Lines changed: 0 additions & 70 deletions
This file was deleted.

packages/crypto/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@
88
* To enable the Web Crypto API and allow `@libp2p/crypto` to work fully, please serve your page over HTTPS.
99
*/
1010

11-
import * as aes from './aes/index.js'
1211
import * as hmac from './hmac/index.js'
1312
import * as keys from './keys/index.js'
1413
import pbkdf2 from './pbkdf2.js'
1514
import randomBytes from './random-bytes.js'
1615

17-
export { aes }
1816
export { hmac }
1917
export { keys }
2018
export { randomBytes }

packages/crypto/src/keys/ed25519-browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ed25519 as ed } from '@noble/curves/ed25519'
2-
import type { Uint8ArrayKeyPair } from './interface'
2+
import type { Uint8ArrayKeyPair } from './interface.js'
33
import type { Uint8ArrayList } from 'uint8arraylist'
44

55
const PUBLIC_KEY_BYTE_LENGTH = 32

packages/crypto/src/keys/index.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@
1010
* For encryption / decryption support, RSA keys should be used.
1111
*/
1212

13-
import 'node-forge/lib/asn1.js'
14-
import 'node-forge/lib/pbe.js'
1513
import { CodeError } from '@libp2p/interface'
16-
// @ts-expect-error types are missing
17-
import forge from 'node-forge/lib/forge.js'
18-
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
1914
import * as Ed25519 from './ed25519-class.js'
2015
import generateEphemeralKeyPair from './ephemeral-keys.js'
2116
import { importer } from './importer.js'
2217
import { keyStretcher } from './key-stretcher.js'
2318
import * as keysPBM from './keys.js'
2419
import * as RSA from './rsa-class.js'
20+
import { importFromPem } from './rsa-utils.js'
2521
import * as Secp256k1 from './secp256k1-class.js'
2622
import type { PrivateKey, PublicKey } from '@libp2p/interface'
2723

@@ -31,6 +27,11 @@ export { keysPBM }
3127

3228
export type KeyTypes = 'RSA' | 'Ed25519' | 'secp256k1'
3329

30+
export { RsaPrivateKey, RsaPublicKey, MAX_RSA_KEY_SIZE } from './rsa-class.js'
31+
export { Ed25519PrivateKey, Ed25519PublicKey } from './ed25519-class.js'
32+
export { Secp256k1PrivateKey, Secp256k1PublicKey } from './secp256k1-class.js'
33+
export type { JWKKeyPair } from './interface.js'
34+
3435
export const supportedKeys = {
3536
rsa: RSA,
3637
ed25519: Ed25519,
@@ -144,12 +145,9 @@ export async function importKey (encryptedKey: string, password: string): Promis
144145
// Ignore and try the old pem decrypt
145146
}
146147

147-
// Only rsa supports pem right now
148-
const key = forge.pki.decryptRsaPrivateKey(encryptedKey, password)
149-
if (key === null) {
150-
throw new CodeError('Cannot read the key, most likely the password is wrong or not a RSA key', 'ERR_CANNOT_DECRYPT_PEM')
148+
if (!encryptedKey.includes('BEGIN')) {
149+
throw new CodeError('Encrypted key was not a libp2p-key or a PEM file', 'ERR_INVALID_IMPORT_FORMAT')
151150
}
152-
let der = forge.asn1.toDer(forge.pki.privateKeyToAsn1(key))
153-
der = uint8ArrayFromString(der.getBytes(), 'ascii')
154-
return supportedKeys.rsa.unmarshalRsaPrivateKey(der)
151+
152+
return importFromPem(encryptedKey, password)
155153
}

packages/crypto/src/keys/jwk2pem.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

packages/crypto/src/keys/rsa-browser.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { CodeError } from '@libp2p/interface'
22
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
3-
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
43
import randomBytes from '../random-bytes.js'
54
import webcrypto from '../webcrypto.js'
6-
import { jwk2pub, jwk2priv } from './jwk2pem.js'
75
import * as utils from './rsa-utils.js'
86
import type { JWKKeyPair } from './interface.js'
97
import type { Uint8ArrayList } from 'uint8arraylist'
@@ -130,33 +128,6 @@ async function derivePublicFromPrivate (jwKey: JsonWebKey): Promise<CryptoKey> {
130128
)
131129
}
132130

133-
/*
134-
135-
RSA encryption/decryption for the browser with webcrypto workaround
136-
"bloody dark magic. webcrypto's why."
137-
138-
Explanation:
139-
- Convert JWK to nodeForge
140-
- Convert msg Uint8Array to nodeForge buffer: ByteBuffer is a "binary-string backed buffer", so let's make our Uint8Array a binary string
141-
- Convert resulting nodeForge buffer to Uint8Array: it returns a binary string, turn that into a Uint8Array
142-
143-
*/
144-
145-
function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array | Uint8ArrayList, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array {
146-
const fkey = pub ? jwk2pub(key) : jwk2priv(key)
147-
const fmsg = uint8ArrayToString(msg instanceof Uint8Array ? msg : msg.subarray(), 'ascii')
148-
const fomsg = handle(fmsg, fkey)
149-
return uint8ArrayFromString(fomsg, 'ascii')
150-
}
151-
152-
export function encrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
153-
return convertKey(key, true, msg, (msg, key) => key.encrypt(msg))
154-
}
155-
156-
export function decrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
157-
return convertKey(key, false, msg, (msg, key) => key.decrypt(msg))
158-
}
159-
160131
export function keySize (jwk: JsonWebKey): number {
161132
if (jwk.kty !== 'RSA') {
162133
throw new CodeError('invalid key type', 'ERR_INVALID_KEY_TYPE')

0 commit comments

Comments
 (0)