diff --git a/README.md b/README.md index 9a38e3ff9..5f5279032 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ This library works on both web browsers and Node.js (currently, Deno is not s | DHKEM (P-256, HKDF-SHA256) | ✅ | ✅ | | | DHKEM (P-384, HKDF-SHA384) | ✅ | ✅ | | | DHKEM (P-521, HKDF-SHA512) | ✅ | ✅ | | -| DHKEM (X25519, HKDF-SHA256) | | | | +| DHKEM (X25519, HKDF-SHA256) | ✅ | ✅ | | | DHKEM (X448, HKDF-SHA512) | | | | ### Key Derivation Functions (KDFs) diff --git a/package-lock.json b/package-lock.json index c59e086c5..0b253fffd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.6.0", "license": "MIT", "dependencies": { - "@stablelib/chacha20poly1305": "^1.0.1" + "@stablelib/chacha20poly1305": "^1.0.1", + "@stablelib/x25519": "^1.0.2" }, "devDependencies": { "@types/jest": "^27.4.1", @@ -1020,6 +1021,11 @@ "@stablelib/int": "^1.0.1" } }, + "node_modules/@stablelib/bytes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/bytes/-/bytes-1.0.1.tgz", + "integrity": "sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ==" + }, "node_modules/@stablelib/chacha": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz", @@ -1052,6 +1058,14 @@ "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==" }, + "node_modules/@stablelib/keyagreement": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz", + "integrity": "sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg==", + "dependencies": { + "@stablelib/bytes": "^1.0.1" + } + }, "node_modules/@stablelib/poly1305": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", @@ -1061,11 +1075,30 @@ "@stablelib/wipe": "^1.0.1" } }, + "node_modules/@stablelib/random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.1.tgz", + "integrity": "sha512-zOh+JHX3XG9MSfIB0LZl/YwPP9w3o6WBiJkZvjPoKKu5LKFW4OLV71vMxWp9qG5T43NaWyn0QQTWgqCdO+yOBQ==", + "dependencies": { + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, "node_modules/@stablelib/wipe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==" }, + "node_modules/@stablelib/x25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/x25519/-/x25519-1.0.2.tgz", + "integrity": "sha512-wTR0t0Bp1HABLFRbYaE3vFLuco2QbAg6QvxBnzi5j9qjhYezWHW7OiCZyaWbt25UkSaoolUUT4Il0nS/2vcbSw==", + "dependencies": { + "@stablelib/keyagreement": "^1.0.1", + "@stablelib/random": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -6816,6 +6849,11 @@ "@stablelib/int": "^1.0.1" } }, + "@stablelib/bytes": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/bytes/-/bytes-1.0.1.tgz", + "integrity": "sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ==" + }, "@stablelib/chacha": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz", @@ -6848,6 +6886,14 @@ "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==" }, + "@stablelib/keyagreement": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/keyagreement/-/keyagreement-1.0.1.tgz", + "integrity": "sha512-VKL6xBwgJnI6l1jKrBAfn265cspaWBPAPEc62VBQrWHLqVgNRE09gQ/AnOEyKUWrrqfD+xSQ3u42gJjLDdMDQg==", + "requires": { + "@stablelib/bytes": "^1.0.1" + } + }, "@stablelib/poly1305": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", @@ -6857,11 +6903,30 @@ "@stablelib/wipe": "^1.0.1" } }, + "@stablelib/random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.1.tgz", + "integrity": "sha512-zOh+JHX3XG9MSfIB0LZl/YwPP9w3o6WBiJkZvjPoKKu5LKFW4OLV71vMxWp9qG5T43NaWyn0QQTWgqCdO+yOBQ==", + "requires": { + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, "@stablelib/wipe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==" }, + "@stablelib/x25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/x25519/-/x25519-1.0.2.tgz", + "integrity": "sha512-wTR0t0Bp1HABLFRbYaE3vFLuco2QbAg6QvxBnzi5j9qjhYezWHW7OiCZyaWbt25UkSaoolUUT4Il0nS/2vcbSw==", + "requires": { + "@stablelib/keyagreement": "^1.0.1", + "@stablelib/random": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", diff --git a/package.json b/package.json index 87f617f5c..9f5adde7d 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "url": "https://github.com/dajiaji/hpke-js/issues" }, "dependencies": { - "@stablelib/chacha20poly1305": "^1.0.1" + "@stablelib/chacha20poly1305": "^1.0.1", + "@stablelib/x25519": "^1.0.2" } } diff --git a/src/cipherSuite.ts b/src/cipherSuite.ts index 45aef36f0..79d94cad8 100644 --- a/src/cipherSuite.ts +++ b/src/cipherSuite.ts @@ -46,6 +46,7 @@ export class CipherSuite { case Kem.DhkemP256HkdfSha256: case Kem.DhkemP384HkdfSha384: case Kem.DhkemP521HkdfSha512: + case Kem.DhkemX25519HkdfSha256: break; default: throw new errors.InvalidParamError('Invalid KEM id'); diff --git a/src/identifiers.ts b/src/identifiers.ts index 55a94252f..85b4a21dc 100644 --- a/src/identifiers.ts +++ b/src/identifiers.ts @@ -18,8 +18,8 @@ export enum Kem { DhkemP384HkdfSha384 = 0x0011, /** DHKEM (P-521, HKDF-SHA512). */ DhkemP521HkdfSha512 = 0x0012, - // /** DHKEM (X25519, HKDF-SHA256) */ - // DhkemX25519HkdfSha256 = 0x0020, + /** DHKEM (X25519, HKDF-SHA256) */ + DhkemX25519HkdfSha256 = 0x0020, // /** DHKEM (X448, HKDF-SHA512) */ // DhkemX448HkdfSha512 = 0x0021, } diff --git a/src/kemContext.ts b/src/kemContext.ts index 1d8fcf2fb..95475dccc 100644 --- a/src/kemContext.ts +++ b/src/kemContext.ts @@ -3,6 +3,7 @@ import type { SenderContextParams } from './interfaces/senderContextParams'; import type { RecipientContextParams } from './interfaces/recipientContextParams'; import { Ec } from './kemPrimitives/ec'; +import { X25519 } from './kemPrimitives/x25519'; import { Kem } from './identifiers'; import { KdfCommon } from './kdfCommon'; import { isCryptoKeyPair, i2Osp, concat, concat3 } from './utils/misc'; @@ -28,10 +29,13 @@ export class KemContext extends KdfCommon { case Kem.DhkemP384HkdfSha384: algHash = { name: 'HMAC', hash: 'SHA-384', length: 384 }; break; - default: - // case Kem.DhkemP521HkdfSha512: + case Kem.DhkemP521HkdfSha512: algHash = { name: 'HMAC', hash: 'SHA-512', length: 512 }; break; + default: + // case Kem.DhkemX25519HkdfSha256: + algHash = { name: 'HMAC', hash: 'SHA-256', length: 256 }; + break; } super(api, suiteId, algHash); @@ -44,11 +48,15 @@ export class KemContext extends KdfCommon { this._prim = new Ec(kem, this, this._api); this._nSecret = 48; break; - default: - // case Kem.DhkemP521HkdfSha512: + case Kem.DhkemP521HkdfSha512: this._prim = new Ec(kem, this, this._api); this._nSecret = 64; break; + default: + // case Kem.DhkemX25519HkdfSha256: + this._prim = new X25519(this); + this._nSecret = 32; + break; } return; } diff --git a/src/kemPrimitives/ec.ts b/src/kemPrimitives/ec.ts index f0fe481ec..2b2cabee7 100644 --- a/src/kemPrimitives/ec.ts +++ b/src/kemPrimitives/ec.ts @@ -28,7 +28,6 @@ const PKCS8_ALG_ID_P_521 = new Uint8Array([ export class Ec implements KemPrimitives { - private _kem: Kem; private _hkdf: KdfCommon; private _api: SubtleCrypto; private _alg: EcKeyGenParams; @@ -43,10 +42,9 @@ export class Ec implements KemPrimitives { private _pkcs8AlgId: Uint8Array; constructor(kem: Kem, hkdf: KdfCommon, api: SubtleCrypto) { - this._kem = kem; this._hkdf = hkdf; this._api = api; - switch (this._kem) { + switch (kem) { case Kem.DhkemP256HkdfSha256: this._alg = { name: 'ECDH', namedCurve: 'P-256' }; this._nPk = 65; diff --git a/src/kemPrimitives/x25519.ts b/src/kemPrimitives/x25519.ts new file mode 100644 index 000000000..3c26a77c8 --- /dev/null +++ b/src/kemPrimitives/x25519.ts @@ -0,0 +1,108 @@ +import { generateKeyPair, scalarMultBase, sharedKey } from '@stablelib/x25519'; + +import type { KemPrimitives } from '../interfaces/kemPrimitives'; +import type { KdfCommon } from '../kdfCommon'; + +import { Kem } from '../identifiers'; +import { i2Osp } from '../utils/misc'; + +import * as consts from '../consts'; + +export class XCryptoKey implements CryptoKey { + + public readonly key: Uint8Array; + public readonly type: 'public' | 'private'; + public readonly extractable: boolean = true; + public readonly algorithm: KeyAlgorithm = { name: 'X25519' }; + public readonly usages: KeyUsage[] = consts.KEM_USAGES; + + constructor(key: Uint8Array, type: 'public' | 'private') { + this.key = key; + this.type = type; + } +} + +export class X25519 implements KemPrimitives { + + private _hkdf: KdfCommon; + private _nPk: number; + private _nSk: number; + + constructor(hkdf: KdfCommon) { + this._hkdf = hkdf; + this._nPk = 32; + this._nSk = 32; + } + + public async serializePublicKey(key: CryptoKey): Promise { + return await this._serializePublicKey(key as XCryptoKey); + } + + public async deserializePublicKey(key: ArrayBuffer): Promise { + return await this._deserializePublicKey(key); + } + + public async derivePublicKey(key: CryptoKey): Promise { + return await this._derivePublicKey(key as XCryptoKey); + } + + public async generateKeyPair(): Promise { + return await this._generateKeyPair(); + } + + public async deriveKeyPair(ikm: ArrayBuffer): Promise { + const dkpPrk = await this._hkdf.labeledExtract(consts.EMPTY, consts.LABEL_DKP_PRK, new Uint8Array(ikm)); + const rawSk = await this._hkdf.labeledExpand(dkpPrk, consts.LABEL_SK, consts.EMPTY, this._nSk); + const sk = new XCryptoKey(new Uint8Array(rawSk), 'private'); + return { + privateKey: sk, + publicKey: await this.derivePublicKey(sk), + }; + } + + public async dh(sk: CryptoKey, pk: CryptoKey): Promise { + return await this._dh(sk as XCryptoKey, pk as XCryptoKey); + } + + private _serializePublicKey(k: XCryptoKey): Promise { + return new Promise((resolve) => { + resolve(k.key.buffer); + }); + } + + private _deserializePublicKey(k: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + if (k.byteLength !== this._nPk) { + reject(new Error('invalid public key for the ciphersuite')); + } else { + resolve(new XCryptoKey(new Uint8Array(k), 'public')); + } + }); + } + + private _derivePublicKey(k: XCryptoKey): Promise { + return new Promise((resolve) => { + resolve(new XCryptoKey(scalarMultBase(k.key), 'public')); + }); + } + + private _generateKeyPair(): Promise { + return new Promise((resolve) => { + const kp = generateKeyPair(); + resolve({ + publicKey: new XCryptoKey(kp.publicKey, 'public'), + privateKey: new XCryptoKey(kp.secretKey, 'private'), + }); + }); + } + + private _dh(sk: XCryptoKey, pk: XCryptoKey): Promise { + return new Promise((resolve, reject) => { + try { + resolve(sharedKey(sk.key, pk.key)); + } catch (e: unknown) { + reject(e); + } + }); + } +} diff --git a/test/cipherSuite.test.ts b/test/cipherSuite.test.ts index 1aff1efe7..e5ae0a161 100644 --- a/test/cipherSuite.test.ts +++ b/test/cipherSuite.test.ts @@ -16,6 +16,44 @@ describe('CipherSuite', () => { } }); + // RFC9180 A.1. + describe('constructor with DhkemX25519HkdfSha256/HkdfSha256/Aes128Gcm', () => { + it('should have ciphersuites', () => { + const suite: CipherSuite = new CipherSuite({ + kem: Kem.DhkemX25519HkdfSha256, + kdf: Kdf.HkdfSha256, + aead: Aead.Aes128Gcm, + }); + + // assert + expect(suite.kem).toEqual(Kem.DhkemX25519HkdfSha256); + expect(suite.kem).toEqual(0x0020); + expect(suite.kdf).toEqual(Kdf.HkdfSha256); + expect(suite.kdf).toEqual(0x0001); + expect(suite.aead).toEqual(Aead.Aes128Gcm); + expect(suite.aead).toEqual(0x0001); + }); + }); + + // RFC9180 A.2. + describe('constructor with DhkemX25519HkdfSha256/HkdfSha256/ChaCha20Poly1305', () => { + it('should have ciphersuites', () => { + const suite: CipherSuite = new CipherSuite({ + kem: Kem.DhkemX25519HkdfSha256, + kdf: Kdf.HkdfSha256, + aead: Aead.Chacha20Poly1305, + }); + + // assert + expect(suite.kem).toEqual(Kem.DhkemX25519HkdfSha256); + expect(suite.kem).toEqual(0x0020); + expect(suite.kdf).toEqual(Kdf.HkdfSha256); + expect(suite.kdf).toEqual(0x0001); + expect(suite.aead).toEqual(Aead.Chacha20Poly1305); + expect(suite.aead).toEqual(0x0003); + }); + }); + // RFC9180 A.3. describe('constructor with DhkemP256HkdfSha256/HkdfSha256/Aes128Gcm', () => { it('should have ciphersuites', () => { @@ -54,6 +92,25 @@ describe('CipherSuite', () => { }); }); + // RFC9180 A.5. + describe('constructor with DhkemP256HkdfSha256/HkdfSha256/ChaCha20Poly1305', () => { + it('should have ciphersuites', () => { + const suite: CipherSuite = new CipherSuite({ + kem: Kem.DhkemP256HkdfSha256, + kdf: Kdf.HkdfSha256, + aead: Aead.Chacha20Poly1305, + }); + + // assert + expect(suite.kem).toEqual(Kem.DhkemP256HkdfSha256); + expect(suite.kem).toEqual(0x0010); + expect(suite.kdf).toEqual(Kdf.HkdfSha256); + expect(suite.kdf).toEqual(0x0001); + expect(suite.aead).toEqual(Aead.Chacha20Poly1305); + expect(suite.aead).toEqual(0x0003); + }); + }); + // RFC9180 A.6. describe('constructor with DhkemP521HkdfSha512/HkdfSha512/Aes256Gcm', () => { it('should have ciphersuites', () => { @@ -73,6 +130,25 @@ describe('CipherSuite', () => { }); }); + // RFC9180 A.7. + describe('constructor with DhkemP256HkdfSha256/HkdfSha256/ExportOnly', () => { + it('should have ciphersuites', () => { + const suite: CipherSuite = new CipherSuite({ + kem: Kem.DhkemP256HkdfSha256, + kdf: Kdf.HkdfSha256, + aead: Aead.ExportOnly, + }); + + // assert + expect(suite.kem).toEqual(Kem.DhkemP256HkdfSha256); + expect(suite.kem).toEqual(0x0010); + expect(suite.kdf).toEqual(Kdf.HkdfSha256); + expect(suite.kdf).toEqual(0x0001); + expect(suite.aead).toEqual(Aead.ExportOnly); + expect(suite.aead).toEqual(0xFFFF); + }); + }); + describe('constructor with invalid KEM id', () => { it('should throw InvalidParamError', () => { expect(() => { @@ -172,6 +248,38 @@ describe('CipherSuite', () => { }); }); + describe('A README example of Base mode (Kem.DhkemX25519HkdfSha256/Kdf.HkdfSha384)', () => { + it('should work normally', async () => { + + // setup + const suite = new CipherSuite({ + kem: Kem.DhkemX25519HkdfSha256, + kdf: Kdf.HkdfSha384, + aead: Aead.Aes128Gcm, + }); + + const rkp = await suite.generateKeyPair(); + + const sender = await suite.createSenderContext({ + recipientPublicKey: rkp.publicKey, + }); + + const recipient = await suite.createRecipientContext({ + recipientKey: rkp, + enc: sender.enc, + }); + + // encrypt + const ct = await sender.seal(new TextEncoder().encode('my-secret-message')); + + // decrypt + const pt = await recipient.open(ct); + + // assert + expect(new TextDecoder().decode(pt)).toEqual('my-secret-message'); + }); + }); + describe('A README example of Base mode (ExportOnly)', () => { it('should work normally', async () => { diff --git a/test/conformance.jsdom.test.ts b/test/conformance.jsdom.test.ts index 29e3d50df..36775cf2b 100644 --- a/test/conformance.jsdom.test.ts +++ b/test/conformance.jsdom.test.ts @@ -57,6 +57,39 @@ describe('RFC9180 conformance (on jsdom)', () => { }); }); + describe('Base/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 0 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('Base/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 0 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('Base/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 0 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); + describe('PSK/DhkemP*/HkdfSha*/Aes*Gcm in test-vectors.json', () => { it('should match demonstrated values', async () => { @@ -90,6 +123,39 @@ describe('RFC9180 conformance (on jsdom)', () => { }); }); + describe('PSK/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 1 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('PSK/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 1 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('PSK/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 1 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); + describe('Auth/DhkemP*/HkdfSha*/Aes*Gcm in test-vectors.json', () => { it('should match demonstrated values', async () => { @@ -123,6 +189,39 @@ describe('RFC9180 conformance (on jsdom)', () => { }); }); + describe('Auth/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 2 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('Auth/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 2 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('Auth/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 2 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); + describe('AuthPSK/DhkemP*/HkdfSha*/Aes*Gcm in test-vectors.json', () => { it('should match demonstrated values', async () => { @@ -156,4 +255,36 @@ describe('RFC9180 conformance (on jsdom)', () => { }); }); + describe('AuthPSK/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 3 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('AuthPSK/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 3 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('AuthPSK/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 3 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); }); diff --git a/test/conformance.test.ts b/test/conformance.test.ts index cc8377721..acc525776 100644 --- a/test/conformance.test.ts +++ b/test/conformance.test.ts @@ -35,7 +35,7 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 0 && v.kem_id < 0x0020 && v.aead_id == 0x0003) { + if (v.mode === 0 && v.kem_id < 0x0020 && v.aead_id === 0x0003) { await tester.test(v); } } @@ -46,7 +46,40 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 0 && v.kem_id < 0x0020 && v.aead_id == 0xFFFF) { + if (v.mode === 0 && v.kem_id < 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); + + describe('Base/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 0 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('Base/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 0 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('Base/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 0 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { await tester.test(v); } } @@ -68,7 +101,7 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 1 && v.kem_id < 0x0020 && v.aead_id == 0x0003) { + if (v.mode === 1 && v.kem_id < 0x0020 && v.aead_id === 0x0003) { await tester.test(v); } } @@ -79,7 +112,40 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 1 && v.kem_id < 0x0020 && v.aead_id == 0xFFFF) { + if (v.mode === 1 && v.kem_id < 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); + + describe('PSK/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 1 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('PSK/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 1 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('PSK/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 1 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { await tester.test(v); } } @@ -101,7 +167,7 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 2 && v.kem_id < 0x0020 && v.aead_id == 0x0003) { + if (v.mode === 2 && v.kem_id < 0x0020 && v.aead_id === 0x0003) { await tester.test(v); } } @@ -112,7 +178,40 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 2 && v.kem_id < 0x0020 && v.aead_id == 0xFFFF) { + if (v.mode === 2 && v.kem_id < 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); + + describe('Auth/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 2 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('Auth/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 2 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('Auth/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 2 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { await tester.test(v); } } @@ -134,7 +233,7 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 3 && v.kem_id < 0x0020 && v.aead_id == 0x0003) { + if (v.mode === 3 && v.kem_id < 0x0020 && v.aead_id === 0x0003) { await tester.test(v); } } @@ -145,11 +244,43 @@ describe('RFC9180 conformance', () => { it('should match demonstrated values', async () => { for (const v of testVectors) { - if (v.mode === 3 && v.kem_id < 0x0020 && v.aead_id == 0xFFFF) { + if (v.mode === 3 && v.kem_id < 0x0020 && v.aead_id === 0xFFFF) { await tester.test(v); } } }); }); + describe('AuthPSK/DhkemX25519/HkdfSha*/Aes*Gcm in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 3 && v.kem_id === 0x0020 && v.aead_id <= 0x0002) { + await tester.test(v); + } + } + }); + }); + + describe('AuthPSK/DhkemX25519/HkdfSha*/ChaCha20Poly1305 in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 3 && v.kem_id === 0x0020 && v.aead_id === 0x0003) { + await tester.test(v); + } + } + }); + }); + + describe('AuthPSK/DhkemX25519/HkdfSha*/ExportOnly in test-vectors.json', () => { + it('should match demonstrated values', async () => { + + for (const v of testVectors) { + if (v.mode === 3 && v.kem_id === 0x0020 && v.aead_id === 0xFFFF) { + await tester.test(v); + } + } + }); + }); }); diff --git a/test/conformanceTester.ts b/test/conformanceTester.ts index 7a1ce742e..c8c416ff5 100644 --- a/test/conformanceTester.ts +++ b/test/conformanceTester.ts @@ -2,6 +2,7 @@ import type { PreSharedKey } from '../src/interfaces/preSharedKey'; import type { TestVector } from './testVector'; import { CipherSuite } from '../src/cipherSuite'; +import { XCryptoKey } from '../src/kemPrimitives/x25519'; import { WebCrypto } from '../src/webCrypto'; import { loadSubtleCrypto } from '../src/webCrypto'; import { @@ -52,11 +53,11 @@ export class ConformanceTester extends WebCrypto { // deriveKeyPair const derivedR = await suite.deriveKeyPair(ikmR.buffer); - const derivedPkRm = await this._api.exportKey('raw', derivedR.publicKey); - expect(new Uint8Array(derivedPkRm)).toEqual(pkRm); + const derivedPkRm = await this.cryptoKeyToBytes(derivedR.publicKey, kemToKeyGenAlgorithm(v.kem_id)); + expect(derivedPkRm).toEqual(pkRm); const derivedE = await suite.deriveKeyPair(ikmE.buffer); - const derivedPkEm = await this._api.exportKey('raw', derivedE.publicKey); - expect(new Uint8Array(derivedPkEm)).toEqual(pkEm); + const derivedPkEm = await this.cryptoKeyToBytes(derivedE.publicKey, kemToKeyGenAlgorithm(v.kem_id)); + expect(derivedPkEm).toEqual(pkEm); const sender = await suite.createSenderContext({ info: info, @@ -102,12 +103,27 @@ export class ConformanceTester extends WebCrypto { this._count++; } - private async bytesToCryptoKeyPair(skm: Uint8Array, pkm: Uint8Array, alg: EcKeyGenParams): Promise { - const pk = await this._api.importKey('raw', pkm, alg, true, ['deriveKey', 'deriveBits']); - const jwk = await this._api.exportKey('jwk', pk); - jwk['d'] = bytesToBase64Url(skm); - const sk = await this._api.importKey('jwk', jwk, alg, true, ['deriveKey', 'deriveBits']); - return { privateKey: sk, publicKey: pk }; + private async bytesToCryptoKeyPair(skm: Uint8Array, pkm: Uint8Array, alg: KeyAlgorithm): Promise { + if (alg.name === 'ECDH') { + const pk = await this._api.importKey('raw', pkm, alg, true, ['deriveKey', 'deriveBits']); + const jwk = await this._api.exportKey('jwk', pk); + jwk['d'] = bytesToBase64Url(skm); + const sk = await this._api.importKey('jwk', jwk, alg, true, ['deriveKey', 'deriveBits']); + return { privateKey: sk, publicKey: pk }; + } + // X25519 + return { + privateKey: new XCryptoKey(skm, 'private'), + publicKey: new XCryptoKey(pkm, 'public'), + }; + } + + private async cryptoKeyToBytes(ck: CryptoKey, alg: KeyAlgorithm): Promise { + if (alg.name === 'ECDH') { + return new Uint8Array(await this._api.exportKey('raw', ck)); + } + // X25519 + return (ck as XCryptoKey).key; } } diff --git a/test/encryptionContext.test.ts b/test/encryptionContext.test.ts index c07445b8e..fb60546d5 100644 --- a/test/encryptionContext.test.ts +++ b/test/encryptionContext.test.ts @@ -220,6 +220,41 @@ describe('CipherSuite', () => { }); }); + describe('createRecipientContext with invalid enc (X25519)', () => { + it('should throw DeserializeError', async () => { + + const suite = new CipherSuite({ + kem: Kem.DhkemX25519HkdfSha256, + kdf: Kdf.HkdfSha256, + aead: Aead.Aes128Gcm, + }); + + const suiteX = new CipherSuite({ + kem: Kem.DhkemP384HkdfSha384, + kdf: Kdf.HkdfSha384, + aead: Aead.Aes128Gcm, + }); + + const rkp = await suite.generateKeyPair(); + const rkpX = await suiteX.generateKeyPair(); + + const senderX = await suiteX.createSenderContext({ + recipientPublicKey: rkpX.publicKey, + }); + + // assert + await expect(suite.createRecipientContext({ + recipientKey: rkp, + enc: senderX.enc, + })).rejects.toThrow(errors.DeserializeError); + + await expect(suite.createRecipientContext({ + recipientKey: rkp, + enc: senderX.enc, + })).rejects.toThrow('invalid public key for the ciphersuite'); + }); + }); + describe('createRecipientContext with invalid recipientKey', () => { it('should throw DecapError', async () => { diff --git a/test/utils.ts b/test/utils.ts index 222b637ab..d3706ca71 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -17,23 +17,27 @@ export function bytesToBase64Url(v: Uint8Array): string { .replace(/=*$/g, ''); } -export function kemToKeyGenAlgorithm(kem: Kem): EcKeyGenParams { +export function kemToKeyGenAlgorithm(kem: Kem): KeyAlgorithm { switch (kem) { case Kem.DhkemP256HkdfSha256: return { name: 'ECDH', namedCurve: 'P-256', - }; + } as KeyAlgorithm; case Kem.DhkemP384HkdfSha384: return { name: 'ECDH', namedCurve: 'P-384', - }; - default: - // case Kem.DhkemP521HkdfSha512: + } as KeyAlgorithm; + case Kem.DhkemP521HkdfSha512: return { name: 'ECDH', namedCurve: 'P-521', + } as KeyAlgorithm; + default: + // case Kem.DhkemX25519HkdfSha256 + return { + name: 'X25519', }; } }