diff --git a/src/exporterContext.ts b/src/exporterContext.ts index 7ee959f6b..60c43242b 100644 --- a/src/exporterContext.ts +++ b/src/exporterContext.ts @@ -2,6 +2,8 @@ import type { Encapsulator } from "./interfaces/encapsulator.ts"; import type { EncryptionContext } from "./interfaces/encryptionContext.ts"; import type { KdfInterface } from "./interfaces/kdfInterface.ts"; +import { emitNotSupported } from "./utils/emitNotSupported.ts"; + import * as consts from "./consts.ts"; import * as errors from "./errors.ts"; @@ -27,14 +29,14 @@ export class ExporterContextImpl implements EncryptionContext { _data: ArrayBuffer, _aad: ArrayBuffer, ): Promise { - return await this._emitError(); + return await emitNotSupported(); } public async open( _data: ArrayBuffer, _aad: ArrayBuffer, ): Promise { - return await this._emitError(); + return await emitNotSupported(); } public async export( @@ -55,12 +57,6 @@ export class ExporterContextImpl implements EncryptionContext { throw new errors.ExportError(e); } } - - private _emitError(): Promise { - return new Promise((_resolve, reject) => { - reject(new errors.NotSupportedError("Not available")); - }); - } } export class RecipientExporterContextImpl extends ExporterContextImpl {} diff --git a/src/interfaces/kemInterface.ts b/src/interfaces/kemInterface.ts index b583f64cf..553940023 100644 --- a/src/interfaces/kemInterface.ts +++ b/src/interfaces/kemInterface.ts @@ -61,6 +61,28 @@ export interface KemInterface { */ deserializePublicKey(key: ArrayBuffer): Promise; + /** + * Serializes a private key as CryptoKey to a byte string of length `Nsk`. + * + * If the error occurred, throws {@link SerializeError}. + * + * @param key A CryptoKey. + * @returns A key as bytes. + * @throws {@link SerializeError} + */ + serializePrivateKey(key: CryptoKey): Promise; + + /** + * Deserializes a private key as a byte string of length `Nsk` to CryptoKey. + * + * If the error occurred, throws {@link DeserializeError}. + * + * @param key A key as bytes. + * @returns A CryptoKey. + * @throws {@link DeserializeError} + */ + deserializePrivateKey(key: ArrayBuffer): Promise; + /** * Imports a public or private key and converts to a {@link CryptoKey}. * diff --git a/src/interfaces/kemPrimitives.ts b/src/interfaces/kemPrimitives.ts index 4fe014a99..a49aa4695 100644 --- a/src/interfaces/kemPrimitives.ts +++ b/src/interfaces/kemPrimitives.ts @@ -14,6 +14,10 @@ export interface KemPrimitives { deserializePublicKey(key: ArrayBuffer): Promise; + serializePrivateKey(key: CryptoKey): Promise; + + deserializePrivateKey(key: ArrayBuffer): Promise; + importKey( format: "raw" | "jwk", key: ArrayBuffer | JsonWebKey, diff --git a/src/kems/dhkem.ts b/src/kems/dhkem.ts index 8c8bf32a3..d46c63fa5 100644 --- a/src/kems/dhkem.ts +++ b/src/kems/dhkem.ts @@ -14,7 +14,6 @@ import * as errors from "../errors.ts"; // b"KEM" const SUITE_ID_HEADER_KEM = new Uint8Array([75, 69, 77, 0, 0]); - // b"eae_prk" const LABEL_EAE_PRK = new Uint8Array([101, 97, 101, 95, 112, 114, 107]); // b"shared_secret" @@ -88,6 +87,24 @@ export class Dhkem extends Algorithm implements KemInterface { } } + public async serializePrivateKey(key: CryptoKey): Promise { + await this._setup(); + try { + return await this._prim.serializePrivateKey(key); + } catch (e: unknown) { + throw new errors.SerializeError(e); + } + } + + public async deserializePrivateKey(key: ArrayBuffer): Promise { + await this._setup(); + try { + return await this._prim.deserializePrivateKey(key); + } catch (e: unknown) { + throw new errors.DeserializeError(e); + } + } + public async importKey( format: "raw" | "jwk", key: ArrayBuffer | JsonWebKey, diff --git a/src/kems/dhkemPrimitives/ec.ts b/src/kems/dhkemPrimitives/ec.ts index c1e40b790..48d9e3c10 100644 --- a/src/kems/dhkemPrimitives/ec.ts +++ b/src/kems/dhkemPrimitives/ec.ts @@ -5,7 +5,7 @@ import { Algorithm } from "../../algorithm.ts"; import { KemId } from "../../identifiers.ts"; import { KEM_USAGES, LABEL_DKP_PRK } from "../../interfaces/kemPrimitives.ts"; import { Bignum } from "../../utils/bignum.ts"; -import { i2Osp } from "../../utils/misc.ts"; +import { base64UrlToBytes, i2Osp } from "../../utils/misc.ts"; import { EMPTY } from "../../consts.ts"; @@ -361,6 +361,28 @@ export class Ec extends Algorithm implements KemPrimitives { } } + public async serializePrivateKey(key: CryptoKey): Promise { + this.checkInit(); + const jwk = await (this._api as SubtleCrypto).exportKey("jwk", key); + if (!("d" in jwk)) { + throw new Error("Not private key"); + } + const ret = base64UrlToBytes(jwk["d"] as string); + // const ret = (await this._api.exportKey('spki', key)).slice(24); + if (ret.byteLength !== this._nSk) { + throw new Error("Invalid length of the key"); + } + return ret; + } + + public async deserializePrivateKey(key: ArrayBuffer): Promise { + this.checkInit(); + if (key.byteLength !== this._nSk) { + throw new Error("Invalid length of the key"); + } + return await this._importRawKey(key, false); + } + public async importKey( format: "raw" | "jwk", key: ArrayBuffer | JsonWebKey, @@ -449,7 +471,6 @@ export class Ec extends Algorithm implements KemPrimitives { const jwk = await (this._api as SubtleCrypto).exportKey("jwk", key); delete jwk["d"]; delete jwk["key_ops"]; - // return await this._api.importKey('jwk', jwk, this._alg, true, KEM_USAGES); return await (this._api as SubtleCrypto).importKey( "jwk", jwk, diff --git a/src/kems/dhkemPrimitives/secp256k1.ts b/src/kems/dhkemPrimitives/secp256k1.ts index 4a62a92fd..038077fe4 100644 --- a/src/kems/dhkemPrimitives/secp256k1.ts +++ b/src/kems/dhkemPrimitives/secp256k1.ts @@ -36,6 +36,14 @@ export class Secp256k1 extends Algorithm implements KemPrimitives { return await this._deserializePublicKey(key); } + public async serializePrivateKey(key: CryptoKey): Promise { + return await this._serializePrivateKey(key as XCryptoKey); + } + + public async deserializePrivateKey(key: ArrayBuffer): Promise { + return await this._deserializePrivateKey(key); + } + public async importKey( format: "raw", key: ArrayBuffer, @@ -102,6 +110,24 @@ export class Secp256k1 extends Algorithm implements KemPrimitives { }); } + private _serializePrivateKey(k: XCryptoKey): Promise { + return new Promise((resolve) => { + resolve(k.key.buffer); + }); + } + + private _deserializePrivateKey(k: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + if (k.byteLength !== this._nSk) { + reject(new Error("Invalid private key for the ciphersuite")); + } else { + resolve( + new XCryptoKey(ALG_NAME, new Uint8Array(k), "private", KEM_USAGES), + ); + } + }); + } + private _importKey(key: ArrayBuffer, isPublic: boolean): Promise { return new Promise((resolve, reject) => { if (isPublic && key.byteLength !== this._nPk) { diff --git a/src/kems/dhkemPrimitives/x25519.ts b/src/kems/dhkemPrimitives/x25519.ts index 611dcef18..09c8c9fed 100644 --- a/src/kems/dhkemPrimitives/x25519.ts +++ b/src/kems/dhkemPrimitives/x25519.ts @@ -37,6 +37,14 @@ export class X25519 extends Algorithm implements KemPrimitives { return await this._deserializePublicKey(key); } + public async serializePrivateKey(key: CryptoKey): Promise { + return await this._serializePrivateKey(key as XCryptoKey); + } + + public async deserializePrivateKey(key: ArrayBuffer): Promise { + return await this._deserializePrivateKey(key); + } + public async importKey( format: "raw" | "jwk", key: ArrayBuffer | JsonWebKey, @@ -107,6 +115,24 @@ export class X25519 extends Algorithm implements KemPrimitives { }); } + private _serializePrivateKey(k: XCryptoKey): Promise { + return new Promise((resolve) => { + resolve(k.key.buffer); + }); + } + + private _deserializePrivateKey(k: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + if (k.byteLength !== this._nSk) { + reject(new Error("Invalid length of the key")); + } else { + resolve( + new XCryptoKey(ALG_NAME, new Uint8Array(k), "private", KEM_USAGES), + ); + } + }); + } + private _importRawKey( key: ArrayBuffer, isPublic: boolean, diff --git a/src/kems/dhkemPrimitives/x448.ts b/src/kems/dhkemPrimitives/x448.ts index c9b040187..809365925 100644 --- a/src/kems/dhkemPrimitives/x448.ts +++ b/src/kems/dhkemPrimitives/x448.ts @@ -37,6 +37,14 @@ export class X448 extends Algorithm implements KemPrimitives { return await this._deserializePublicKey(key); } + public async serializePrivateKey(key: CryptoKey): Promise { + return await this._serializePrivateKey(key as XCryptoKey); + } + + public async deserializePrivateKey(key: ArrayBuffer): Promise { + return await this._deserializePrivateKey(key); + } + public async importKey( format: "raw" | "jwk", key: ArrayBuffer | JsonWebKey, @@ -107,6 +115,24 @@ export class X448 extends Algorithm implements KemPrimitives { }); } + private _serializePrivateKey(k: XCryptoKey): Promise { + return new Promise((resolve) => { + resolve(k.key.buffer); + }); + } + + private _deserializePrivateKey(k: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + if (k.byteLength !== this._nSk && k.byteLength !== this._nSk + 1) { + reject(new Error(`Invalid length of the key: ${new Uint8Array(k)}`)); + } else { + resolve( + new XCryptoKey(ALG_NAME, new Uint8Array(k), "private", KEM_USAGES), + ); + } + }); + } + private _importRawKey( key: ArrayBuffer, isPublic: boolean, @@ -115,7 +141,10 @@ export class X448 extends Algorithm implements KemPrimitives { if (isPublic && key.byteLength !== this._nPk) { reject(new Error("Invalid public key for the ciphersuite")); } - if (!isPublic && key.byteLength !== this._nSk) { + if ( + !isPublic && + (key.byteLength !== this._nSk && key.byteLength !== this._nSk + 1) + ) { reject(new Error("Invalid private key for the ciphersuite")); } resolve( diff --git a/src/utils/emitNotSupported.ts b/src/utils/emitNotSupported.ts new file mode 100644 index 000000000..d8b0c51ad --- /dev/null +++ b/src/utils/emitNotSupported.ts @@ -0,0 +1,7 @@ +import { NotSupportedError } from "../errors.ts"; + +export function emitNotSupported(): Promise { + return new Promise((_resolve, reject) => { + reject(new NotSupportedError("Not supported")); + }); +} diff --git a/test/conformanceTester.ts b/test/conformanceTester.ts index d107309ec..7ae1e8241 100644 --- a/test/conformanceTester.ts +++ b/test/conformanceTester.ts @@ -51,6 +51,14 @@ export class ConformanceTester { privateKey: await suite.kem.importKey("raw", skRm, false), publicKey: await suite.kem.importKey("raw", pkRm, true), }; + + const dSkR = await suite.kem.deserializePrivateKey(skRm); + const dPkR = await suite.kem.deserializePublicKey(pkRm); + const skRm2 = await suite.kem.serializePrivateKey(dSkR); + const pkRm2 = await suite.kem.serializePublicKey(dPkR); + assertEquals(skRm, new Uint8Array(skRm2)); + assertEquals(pkRm, new Uint8Array(pkRm2)); + const ekp = { privateKey: await suite.kem.importKey("raw", skEm, false), publicKey: await suite.kem.importKey("raw", pkEm), // true can be omitted diff --git a/test/kemContext.test.ts b/test/kemContext.test.ts index 6e716c87b..5586b2bc7 100644 --- a/test/kemContext.test.ts +++ b/test/kemContext.test.ts @@ -374,10 +374,6 @@ describe("serialize/deserializePublicKey", () => { describe("with invalid parameters", () => { it("should throw SerializeError on serializePublicKey with a public key for X25519", async () => { - if (!isDeno()) { - return; - } - // assert const ctx = new DhkemX25519HkdfSha256(); const kp = await ctx.generateKeyPair(); @@ -389,17 +385,121 @@ describe("serialize/deserializePublicKey", () => { }); it("should throw DeserializeError on deserializePublicKey with DhkemP256HkdfSha256", async () => { - if (!isDeno()) { + // assert + const kemContext = new DhkemP256HkdfSha256(); + const cryptoApi = await loadCrypto(); + const rawKey = new Uint8Array(32); + cryptoApi.getRandomValues(rawKey); + await assertRejects( + () => kemContext.deserializePublicKey(rawKey.buffer), + errors.DeserializeError, + ); + }); + }); +}); + +describe("serialize/deserializePrivateKey", () => { + describe("with valid parameters", () => { + it("should return a proper instance with DhkemP256HkdfSha256", async () => { + if (isDeno()) { return; } + // assert + const kemContext = new DhkemP256HkdfSha256(); + const kp = await kemContext.generateKeyPair(); + const bPrivKey = await kemContext.serializePrivateKey(kp.privateKey); + const privKey = await kemContext.deserializePrivateKey(bPrivKey); + assertEquals(privKey.type, "private"); + assertEquals(privKey.extractable, true); + assertEquals(privKey.algorithm.name, "ECDH"); + // assertEquals(pubKey.algorithm.namedCurve, "P-256"); + assertEquals(privKey.usages.length, 1); + assertEquals(privKey.usages[0], "deriveBits"); + }); + + it("should return a proper instance with DhkemP384HkdfSha384", async () => { + if (isDeno()) { + return; + } + + // assert + const kemContext = new DhkemP384HkdfSha384(); + const kp = await kemContext.generateKeyPair(); + const bPrivKey = await kemContext.serializePrivateKey(kp.privateKey); + const privKey = await kemContext.deserializePrivateKey(bPrivKey); + assertEquals(privKey.type, "private"); + assertEquals(privKey.extractable, true); + assertEquals(privKey.algorithm.name, "ECDH"); + assertEquals(privKey.usages.length, 1); + assertEquals(privKey.usages[0], "deriveBits"); + }); + + it("should return a proper instance with DhkemP521HkdfSha512", async () => { + if (isDeno()) { + return; + } + + // assert + const kemContext = new DhkemP521HkdfSha512(); + const kp = await kemContext.generateKeyPair(); + const bPrivKey = await kemContext.serializePrivateKey(kp.privateKey); + const privKey = await kemContext.deserializePrivateKey(bPrivKey); + assertEquals(privKey.type, "private"); + assertEquals(privKey.extractable, true); + assertEquals(privKey.algorithm.name, "ECDH"); + // assertEquals(pubKey.algorithm.namedCurve, "P-256"); + assertEquals(privKey.usages.length, 1); + assertEquals(privKey.usages[0], "deriveBits"); + }); + + it("should return a proper instance with DhkemX25519HkdfSha256", async () => { + // assert + const kemContext = new DhkemX25519HkdfSha256(); + const kp = await kemContext.generateKeyPair(); + const bPrivKey = await kemContext.serializePrivateKey(kp.privateKey); + const privKey = await kemContext.deserializePrivateKey(bPrivKey); + assertEquals(privKey.type, "private"); + assertEquals(privKey.extractable, true); + assertEquals(privKey.algorithm.name, "X25519"); + assertEquals(privKey.usages.length, 1); + assertEquals(privKey.usages[0], "deriveBits"); + }); + + it("should return a proper instance with DhkemX448HkdfSha512", async () => { + // assert + const kemContext = new DhkemX448HkdfSha512(); + const kp = await kemContext.generateKeyPair(); + const bPrivKey = await kemContext.serializePrivateKey(kp.privateKey); + const privKey = await kemContext.deserializePrivateKey(bPrivKey); + assertEquals(privKey.type, "private"); + assertEquals(privKey.extractable, true); + assertEquals(privKey.algorithm.name, "X448"); + assertEquals(privKey.usages.length, 1); + assertEquals(privKey.usages[0], "deriveBits"); + }); + }); + + describe("with invalid parameters", () => { + it("should throw SerializeError on serializePrivateKey with a public key for X25519", async () => { + // assert + const ctx = new DhkemX25519HkdfSha256(); + const kp = await ctx.generateKeyPair(); + const kemContext = new DhkemP256HkdfSha256(); + await assertRejects( + () => kemContext.serializePrivateKey(kp.privateKey), + errors.SerializeError, + ); + }); + + it("should throw DeserializeError on deserializePrivateKey with DhkemP256HkdfSha256", async () => { // assert const kemContext = new DhkemP256HkdfSha256(); const cryptoApi = await loadCrypto(); - const rawKey = new Uint8Array(32); + const rawKey = new Uint8Array(65); cryptoApi.getRandomValues(rawKey); await assertRejects( - () => kemContext.deserializePublicKey(rawKey.buffer), + () => kemContext.deserializePrivateKey(rawKey), errors.DeserializeError, ); }); diff --git a/x/hybridkem-x25519-kyber768/dnt.ts b/x/hybridkem-x25519-kyber768/dnt.ts deleted file mode 100644 index 9924a2fc7..000000000 --- a/x/hybridkem-x25519-kyber768/dnt.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { build, emptyDir } from "dnt"; - -await emptyDir("./npm"); - -await build({ - entryPoints: ["./mod.ts"], - outDir: "./npm", - typeCheck: false, - test: true, - declaration: true, - scriptModule: "umd", - importMap: "./deno.json", - compilerOptions: { - lib: ["es2021", "dom"], - }, - shims: { - deno: "dev", - }, - package: { - name: "@hpke/hybridkem-x25519-kyber768", - version: Deno.args[0], - description: - "A Hybrid Public Key Encryption (HPKE) module extension for a hybrid qost-quantum KEM which is the parallel combination of DHKEM(X25519, HKDF-SHA256) and Kyber768 (EXPERIMENTAL)", - repository: { - type: "git", - url: "git+https://github.com/dajiaji/hpke-js.git", - }, - homepage: "https://github.com/dajiaji/hpke-js#readme", - license: "MIT", - main: "./script/x/hybridkem-x25519-kyber768/mod.js", - types: "./script/x/hybridkem-x25519-kyber768/mod.d.ts", - exports: { - ".": { - "import": "./esm/x/hybridkem-x25519-kyber768/mod.js", - "require": "./script/x/hybridkem-x25519-kyber768/mod.js", - }, - "./package.json": "./package.json", - }, - keywords: [ - "dh", - "encryption", - "hkdf", - "hpke", - "kyber", - "post-quantum", - "security", - "x25519", - ], - engines: { - "node": ">=16.0.0", - }, - author: "Ajitomi Daisuke", - bugs: { - url: "https://github.com/dajiaji/hpke-js/issues", - }, - }, -}); - -// post build steps -Deno.copyFileSync("../../LICENSE", "npm/LICENSE"); -Deno.copyFileSync("README.md", "npm/README.md");