Skip to content

Commit

Permalink
fix!: remove node-forge dependency from @libp2p/crypto (#2355)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
achingbrain committed Jan 12, 2024
1 parent ddaa59a commit 856ccd7
Show file tree
Hide file tree
Showing 24 changed files with 515 additions and 829 deletions.
19 changes: 6 additions & 13 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@
"types": "./src/index.d.ts",
"import": "./dist/src/index.js"
},
"./aes": {
"types": "./dist/src/aes/index.d.ts",
"import": "./dist/src/aes/index.js"
},
"./hmac": {
"types": "./dist/src/hmac/index.d.ts",
"import": "./dist/src/hmac/index.js"
Expand All @@ -69,10 +65,7 @@
"parserOptions": {
"project": true,
"sourceType": "module"
},
"ignorePatterns": [
"src/*.d.ts"
]
}
},
"scripts": {
"clean": "aegir clean",
Expand All @@ -92,11 +85,11 @@
"dependencies": {
"@libp2p/interface": "^1.1.1",
"@noble/curves": "^1.1.0",
"@noble/hashes": "^1.3.1",
"@noble/hashes": "^1.3.3",
"asn1js": "^3.0.5",
"multiformats": "^13.0.0",
"node-forge": "^1.1.0",
"protons-runtime": "^5.0.0",
"uint8arraylist": "^2.4.3",
"uint8arraylist": "^2.4.7",
"uint8arrays": "^5.0.0"
},
"devDependencies": {
Expand All @@ -106,12 +99,12 @@
"protons": "^7.3.0"
},
"browser": {
"./dist/src/aes/ciphers.js": "./dist/src/aes/ciphers-browser.js",
"./dist/src/ciphers/aes-gcm.js": "./dist/src/ciphers/aes-gcm.browser.js",
"./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js",
"./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js",
"./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js",
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js",
"./dist/src/keys/secp256k1.js": "./dist/src/keys/secp256k1-browser.js"
"./dist/src/keys/secp256k1.js": "./dist/src/keys/secp256k1-browser.js",
"./dist/src/webcrypto.js": "./dist/src/webcrypto-browser.js"
}
}
15 changes: 0 additions & 15 deletions packages/crypto/src/aes/cipher-mode.ts

This file was deleted.

31 changes: 0 additions & 31 deletions packages/crypto/src/aes/ciphers-browser.ts

This file was deleted.

4 changes: 0 additions & 4 deletions packages/crypto/src/aes/ciphers.ts

This file was deleted.

70 changes: 0 additions & 70 deletions packages/crypto/src/aes/index.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
* To enable the Web Crypto API and allow `@libp2p/crypto` to work fully, please serve your page over HTTPS.
*/

import * as aes from './aes/index.js'
import * as hmac from './hmac/index.js'
import * as keys from './keys/index.js'
import pbkdf2 from './pbkdf2.js'
import randomBytes from './random-bytes.js'

export { aes }
export { hmac }
export { keys }
export { randomBytes }
Expand Down
2 changes: 1 addition & 1 deletion packages/crypto/src/keys/ed25519-browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ed25519 as ed } from '@noble/curves/ed25519'
import type { Uint8ArrayKeyPair } from './interface'
import type { Uint8ArrayKeyPair } from './interface.js'
import type { Uint8ArrayList } from 'uint8arraylist'

const PUBLIC_KEY_BYTE_LENGTH = 32
Expand Down
22 changes: 10 additions & 12 deletions packages/crypto/src/keys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,14 @@
* For encryption / decryption support, RSA keys should be used.
*/

import 'node-forge/lib/asn1.js'
import 'node-forge/lib/pbe.js'
import { CodeError } from '@libp2p/interface'
// @ts-expect-error types are missing
import forge from 'node-forge/lib/forge.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as Ed25519 from './ed25519-class.js'
import generateEphemeralKeyPair from './ephemeral-keys.js'
import { importer } from './importer.js'
import { keyStretcher } from './key-stretcher.js'
import * as keysPBM from './keys.js'
import * as RSA from './rsa-class.js'
import { importFromPem } from './rsa-utils.js'
import * as Secp256k1 from './secp256k1-class.js'
import type { PrivateKey, PublicKey } from '@libp2p/interface'

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

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

export { RsaPrivateKey, RsaPublicKey, MAX_RSA_KEY_SIZE } from './rsa-class.js'
export { Ed25519PrivateKey, Ed25519PublicKey } from './ed25519-class.js'
export { Secp256k1PrivateKey, Secp256k1PublicKey } from './secp256k1-class.js'
export type { JWKKeyPair } from './interface.js'

export const supportedKeys = {
rsa: RSA,
ed25519: Ed25519,
Expand Down Expand Up @@ -144,12 +145,9 @@ export async function importKey (encryptedKey: string, password: string): Promis
// Ignore and try the old pem decrypt
}

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

return importFromPem(encryptedKey, password)
}
21 changes: 0 additions & 21 deletions packages/crypto/src/keys/jwk2pem.ts

This file was deleted.

29 changes: 0 additions & 29 deletions packages/crypto/src/keys/rsa-browser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { CodeError } from '@libp2p/interface'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import randomBytes from '../random-bytes.js'
import webcrypto from '../webcrypto.js'
import { jwk2pub, jwk2priv } from './jwk2pem.js'
import * as utils from './rsa-utils.js'
import type { JWKKeyPair } from './interface.js'
import type { Uint8ArrayList } from 'uint8arraylist'
Expand Down Expand Up @@ -130,33 +128,6 @@ async function derivePublicFromPrivate (jwKey: JsonWebKey): Promise<CryptoKey> {
)
}

/*
RSA encryption/decryption for the browser with webcrypto workaround
"bloody dark magic. webcrypto's why."
Explanation:
- Convert JWK to nodeForge
- Convert msg Uint8Array to nodeForge buffer: ByteBuffer is a "binary-string backed buffer", so let's make our Uint8Array a binary string
- Convert resulting nodeForge buffer to Uint8Array: it returns a binary string, turn that into a Uint8Array
*/

function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array | Uint8ArrayList, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array {
const fkey = pub ? jwk2pub(key) : jwk2priv(key)
const fmsg = uint8ArrayToString(msg instanceof Uint8Array ? msg : msg.subarray(), 'ascii')
const fomsg = handle(fmsg, fkey)
return uint8ArrayFromString(fomsg, 'ascii')
}

export function encrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
return convertKey(key, true, msg, (msg, key) => key.encrypt(msg))
}

export function decrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
return convertKey(key, false, msg, (msg, key) => key.decrypt(msg))
}

export function keySize (jwk: JsonWebKey): number {
if (jwk.kty !== 'RSA') {
throw new CodeError('invalid key type', 'ERR_INVALID_KEY_TYPE')
Expand Down
Loading

0 comments on commit 856ccd7

Please sign in to comment.