diff --git a/packages/cryptography/CHANGELOG.md b/packages/cryptography/CHANGELOG.md index a6bc460df92..b6531df0d11 100644 --- a/packages/cryptography/CHANGELOG.md +++ b/packages/cryptography/CHANGELOG.md @@ -9,10 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial release ([#10282](https://github.com/MetaMask/core/pull/10282), [#10431](https://github.com/MetaMask/core/pull/10431), [#10403](https://github.com/MetaMask/core/pull/10403), [#10468](https://github.com/MetaMask/core/pull/10468)) +- Initial release ([#10282](https://github.com/MetaMask/core/pull/10282), [#10431](https://github.com/MetaMask/core/pull/10431), [#10403](https://github.com/MetaMask/core/pull/10403), [#10468](https://github.com/MetaMask/core/pull/10468), [#10503](https://github.com/MetaMask/core/pull/10503)) - Add `sha256`, `sha384`, and `sha512` functions for computing SHA digests - Add `hmacSha256`, `hmacSha384`, and `hmacSha512` functions for computing HMAC digests - Add `pbkdf2Sha256`, `pbkdf2Sha384`, and `pbkdf2Sha512` functions for key derivation - Add `hkdfSha256`, `hkdfSha384`, and `hkdfSha512` functions for key derivation + - Add `getPublicKey` and `getSharedSecret` functions for X25519 key derivation exported via `@metamask/cryptography/x25519` [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/cryptography/package.json b/packages/cryptography/package.json index 6e7e184f1c8..8c87f3de4d7 100644 --- a/packages/cryptography/package.json +++ b/packages/cryptography/package.json @@ -25,6 +25,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./x25519": { + "types": "./dist/x25519.d.ts", + "default": "./dist/x25519.js" + }, "./package.json": "./package.json" }, "publishConfig": { diff --git a/packages/cryptography/src/utils.ts b/packages/cryptography/src/utils.ts new file mode 100644 index 00000000000..db41cc9d0bc --- /dev/null +++ b/packages/cryptography/src/utils.ts @@ -0,0 +1,12 @@ +/** + * Convert a `BufferSource` to a `Uint8Array` view over the same bytes. + * + * @param source - The `ArrayBuffer`, typed array, or `DataView` to convert. + * @returns A `Uint8Array` sharing memory with the source. + */ +export function toUint8Array(source: BufferSource): Uint8Array { + if (source instanceof ArrayBuffer) { + return new Uint8Array(source); + } + return new Uint8Array(source.buffer, source.byteOffset, source.byteLength); +} diff --git a/packages/cryptography/src/x25519.test.ts b/packages/cryptography/src/x25519.test.ts new file mode 100644 index 00000000000..08088809854 --- /dev/null +++ b/packages/cryptography/src/x25519.test.ts @@ -0,0 +1,137 @@ +import { bytesToHex, hexToBytes } from '@metamask/utils'; + +import { getPublicKey, getSharedSecret } from './x25519.js'; + +const privateKey = hexToBytes( + '0x4a78ac42b72f1232d99257d03675b6268906361f902e85ef9f407270b376b271', +); +const publicKey = hexToBytes( + '0x3131ecda5b9fb0afed66c842197b7eaf063a2e1ceebf60d206c5c11c89916d6d', +); +const publicKey2 = hexToBytes( + '0x7580f1903245d94336767cafcb781a06507b6a8f889c471c2aa348e01bc4f94b', +); +const sharedSecret = hexToBytes( + '0xdccc8b748350104639ac6bf67a1b6e7698dcd007de5cc7c6e010b0185ceade52', +); + +// RFC 7748 Section 6.1 test vectors +// https://datatracker.ietf.org/doc/html/rfc7748#section-6.1 +const rfcAlicePrivateKey = hexToBytes( + '0x77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a', +); +const rfcAlicePublicKey = hexToBytes( + '0x8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a', +); +const rfcBobPrivateKey = hexToBytes( + '0x5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb', +); +const rfcBobPublicKey = hexToBytes( + '0xde9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f', +); +const rfcSharedSecret = + '0x4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742'; + +describe('getPublicKey', () => { + it('derives a public key', async () => { + const pubKey = await getPublicKey(privateKey); + expect(bytesToHex(pubKey)).toBe(bytesToHex(publicKey)); + }); + + it('derives Alice public key from her private key', async () => { + const pubKey = await getPublicKey(rfcAlicePrivateKey); + expect(bytesToHex(pubKey)).toBe(bytesToHex(rfcAlicePublicKey)); + }); + + it('derives Bob public key from his private key', async () => { + const pubKey = await getPublicKey(rfcBobPrivateKey); + expect(bytesToHex(pubKey)).toBe(bytesToHex(rfcBobPublicKey)); + }); + + it('accepts an ArrayBuffer private key', async () => { + const pubKey = await getPublicKey(rfcAlicePrivateKey.buffer); + expect(bytesToHex(pubKey)).toBe(bytesToHex(rfcAlicePublicKey)); + }); + + it('accepts a DataView private key', async () => { + const pubKey = await getPublicKey(new DataView(rfcAlicePrivateKey.buffer)); + expect(bytesToHex(pubKey)).toBe(bytesToHex(rfcAlicePublicKey)); + }); + + it('throws if the private key is too short', async () => { + await expect(getPublicKey(new Uint8Array(31))).rejects.toThrow( + 'Invalid private key length: Private key must be exactly 32 bytes for X25519.', + ); + }); + + it('throws if the private key is too long', async () => { + await expect(getPublicKey(new Uint8Array(33))).rejects.toThrow( + 'Invalid private key length: Private key must be exactly 32 bytes for X25519.', + ); + }); +}); + +describe('getSharedSecret', () => { + it('computes a shared secret', async () => { + const shared = await getSharedSecret(privateKey, publicKey2); + expect(bytesToHex(shared)).toBe(bytesToHex(sharedSecret)); + }); + + it('computes the shared secret from Alice private key and Bob public key', async () => { + const shared = await getSharedSecret(rfcAlicePrivateKey, rfcBobPublicKey); + expect(bytesToHex(shared)).toBe(rfcSharedSecret); + }); + + it('computes the shared secret from Bob private key and Alice public key', async () => { + const shared = await getSharedSecret(rfcBobPrivateKey, rfcAlicePublicKey); + expect(bytesToHex(shared)).toBe(rfcSharedSecret); + }); + + it('accepts an ArrayBuffer private and public key', async () => { + const shared = await getSharedSecret( + rfcAlicePrivateKey.buffer, + rfcBobPublicKey.buffer, + ); + expect(bytesToHex(shared)).toBe(rfcSharedSecret); + }); + + it('accepts a DataView private and public key', async () => { + const shared = await getSharedSecret( + new DataView(rfcAlicePrivateKey.buffer), + new DataView(rfcBobPublicKey.buffer), + ); + expect(bytesToHex(shared)).toBe(rfcSharedSecret); + }); + + it('throws if the private key is too short', async () => { + await expect( + getSharedSecret(new Uint8Array(31), rfcBobPublicKey), + ).rejects.toThrow( + 'Invalid private key length: Private key must be exactly 32 bytes for X25519.', + ); + }); + + it('throws if the private key is too long', async () => { + await expect( + getSharedSecret(new Uint8Array(33), rfcBobPublicKey), + ).rejects.toThrow( + 'Invalid private key length: Private key must be exactly 32 bytes for X25519.', + ); + }); + + it('throws if the public key is too short', async () => { + await expect( + getSharedSecret(rfcAlicePrivateKey, new Uint8Array(31)), + ).rejects.toThrow( + 'Invalid public key length: Public key must be exactly 32 bytes for X25519.', + ); + }); + + it('throws if the public key is too long', async () => { + await expect( + getSharedSecret(rfcAlicePrivateKey, new Uint8Array(33)), + ).rejects.toThrow( + 'Invalid public key length: Public key must be exactly 32 bytes for X25519.', + ); + }); +}); diff --git a/packages/cryptography/src/x25519.ts b/packages/cryptography/src/x25519.ts new file mode 100644 index 00000000000..bb6834d2039 --- /dev/null +++ b/packages/cryptography/src/x25519.ts @@ -0,0 +1,96 @@ +import { toUint8Array } from './utils.js'; + +const X25519_KEY_LENGTH = 32; + +// https://www.rfc-editor.org/rfc/rfc7748#section-4.1 +const X25519_BASE_POINT = new Uint8Array(32); +X25519_BASE_POINT[0] = 9; + +// https://www.rfc-editor.org/rfc/rfc8410#section-7 +// https://github.com/nodejs/node/blob/main/test/parallel/test-webcrypto-export-import-cfrg.js +const X25519_PKCS8_HEADER = new Uint8Array([ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, + 0x22, 0x04, 0x20, +]); + +/** + * Perform scalar multiplication of a point by a private key, + * specified as the X25519 function in RFC 7748, Section 5. + * + * @param privateKey - The 32-byte X25519 private key (scalar). + * @param publicKey - The 32-byte X25519 public key (u-coordinate). + * @returns The 32-byte result of the scalar multiplication. + */ +async function scalarMultiply( + privateKey: BufferSource, + publicKey: BufferSource, +): Promise { + if (privateKey.byteLength !== X25519_KEY_LENGTH) { + throw new Error( + `Invalid private key length: Private key must be exactly ${X25519_KEY_LENGTH} bytes for X25519.`, + ); + } + + if (publicKey.byteLength !== X25519_KEY_LENGTH) { + throw new Error( + `Invalid public key length: Public key must be exactly ${X25519_KEY_LENGTH} bytes for X25519.`, + ); + } + + // The WebCrypto API expects private keys to be in PKCS8 format. + const pkcs8 = new Uint8Array(X25519_PKCS8_HEADER.length + X25519_KEY_LENGTH); + pkcs8.set(X25519_PKCS8_HEADER); + pkcs8.set(toUint8Array(privateKey), X25519_PKCS8_HEADER.length); + + const subtlePrivateKey = await globalThis.crypto.subtle.importKey( + 'pkcs8', + pkcs8, + { name: 'X25519' }, + false, + ['deriveBits'], + ); + + const subtlePublicKey = await globalThis.crypto.subtle.importKey( + 'raw', + publicKey, + { name: 'X25519' }, + false, + [], + ); + + const result = await globalThis.crypto.subtle.deriveBits( + { name: 'X25519', public: subtlePublicKey }, + subtlePrivateKey, + X25519_KEY_LENGTH * 8, + ); + + return new Uint8Array(result); +} + +/** + * Derive the X25519 public key corresponding to a private key. + * + * @param privateKey - The 32-byte X25519 private key. + * @returns The 32-byte X25519 public key. + */ +export async function getPublicKey( + privateKey: BufferSource, +): Promise { + // X25519 public keys are derived as X25519(k, 9) + return scalarMultiply(privateKey, X25519_BASE_POINT); +} + +/** + * Compute the X25519 shared secret given a private key and a peer's public key. + * + * @param privateKey - The 32-byte X25519 private key. + * @param publicKey - The 32-byte X25519 public key of the peer. + * @returns The 32-byte shared secret. + */ +export async function getSharedSecret( + privateKey: BufferSource, + publicKey: BufferSource, +): Promise { + // X25519 shared secrets are derived as X25519(a, K_b) + return scalarMultiply(privateKey, publicKey); +}