Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/cryptography/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
4 changes: 4 additions & 0 deletions packages/cryptography/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 12 additions & 0 deletions packages/cryptography/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
137 changes: 137 additions & 0 deletions packages/cryptography/src/x25519.test.ts
Original file line number Diff line number Diff line change
@@ -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.',
);
});
});
96 changes: 96 additions & 0 deletions packages/cryptography/src/x25519.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array> {
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.
Comment thread
Mrtenz marked this conversation as resolved.
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<Uint8Array> {
// 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<Uint8Array> {
// X25519 shared secrets are derived as X25519(a, K_b)
return scalarMultiply(privateKey, publicKey);
}
Loading