From a0cf28914d6e0e11567a24ea3508c3ca231bd754 Mon Sep 17 00:00:00 2001 From: microshine Date: Tue, 28 May 2024 22:13:26 +0200 Subject: [PATCH] feat: Add support for Ed25519 and X25519 algorithms --- src/mechs/ed25519/crypto.ts | 120 +++++++++++++++++ src/mechs/ed25519/crypto_key.ts | 21 +++ src/mechs/ed25519/ed25519.ts | 37 ++++++ src/mechs/ed25519/index.ts | 4 + src/mechs/ed25519/private_key.ts | 23 ++++ src/mechs/ed25519/public_key.ts | 18 +++ src/mechs/ed25519/x25519.ts | 54 ++++++++ src/mechs/index.ts | 1 + src/subtle.ts | 4 + test/crypto.ts | 222 +++++++++++++++++++++++++++++++ 10 files changed, 504 insertions(+) create mode 100644 src/mechs/ed25519/crypto.ts create mode 100644 src/mechs/ed25519/crypto_key.ts create mode 100644 src/mechs/ed25519/ed25519.ts create mode 100644 src/mechs/ed25519/index.ts create mode 100644 src/mechs/ed25519/private_key.ts create mode 100644 src/mechs/ed25519/public_key.ts create mode 100644 src/mechs/ed25519/x25519.ts diff --git a/src/mechs/ed25519/crypto.ts b/src/mechs/ed25519/crypto.ts new file mode 100644 index 0000000..ba54151 --- /dev/null +++ b/src/mechs/ed25519/crypto.ts @@ -0,0 +1,120 @@ +import crypto from "crypto"; +import { AsnConvert } from "@peculiar/asn1-schema"; +import { Convert } from "pvtsutils"; +import * as core from "webcrypto-core"; +import { Ed25519CryptoKey } from "./crypto_key"; +import { Ed25519PrivateKey } from "./private_key"; +import { Ed25519PublicKey } from "./public_key"; +import { CryptoKey } from "../../keys"; + +export class Ed25519Crypto { + public static privateKeyUsages: KeyUsage[] = ["sign", "deriveBits", "deriveKey"]; + public static publicKeyUsages: KeyUsage[] = ["verify"]; + + public static async generateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise { + const type = algorithm.name.toLowerCase() as "ed25519"; + const keys = crypto.generateKeyPairSync(type, { + publicKeyEncoding: { + format: "pem", + type: "spki", + }, + privateKeyEncoding: { + format: "pem", + type: "pkcs8", + }, + }); + + const keyAlg = { + name: type === "ed25519" ? "Ed25519" : "X25519", + }; + const privateKeyUsages = keyUsages.filter((usage) => this.privateKeyUsages.includes(usage)); + const publicKeyUsages = keyUsages.filter((usage) => this.publicKeyUsages.includes(usage)); + return { + privateKey: new Ed25519PrivateKey(keyAlg, extractable, privateKeyUsages, keys.privateKey), + publicKey: new Ed25519PublicKey(keyAlg, true, publicKeyUsages, keys.publicKey), + }; + } + + public static async sign(algorithm: Algorithm, key: Ed25519PrivateKey, data: Uint8Array): Promise { + const signature = crypto.sign(null, Buffer.from(data), key.data); + + return core.BufferSourceConverter.toArrayBuffer(signature); + } + + public static async verify(algorithm: Algorithm, key: Ed25519PublicKey, signature: Uint8Array, data: Uint8Array): Promise { + return crypto.verify(null, Buffer.from(data), key.data, signature); + } + + public static async exportKey(format: KeyFormat, key: Ed25519CryptoKey): Promise { + switch (format) { + case "jwk": + return key.toJWK(); + case "pkcs8": { + return core.PemConverter.toArrayBuffer(key.data.toString()); + } + case "spki": { + return core.PemConverter.toArrayBuffer(key.data.toString()); + } + case "raw": { + const jwk = key.toJWK(); + return Convert.FromBase64Url(jwk.x!); + } + default: + return Promise.reject(new core.OperationError("format: Must be 'jwk', 'raw', pkcs8' or 'spki'")); + } + } + + public static async importKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise { + switch (format) { + case "jwk": { + const jwk = keyData as JsonWebKey; + if (jwk.d) { + // private key + const privateData = new core.asn1.EdPrivateKey(); + privateData.value = core.BufferSourceConverter.toArrayBuffer(Buffer.from(jwk.d, "base64url")); + const pkcs8 = new core.asn1.PrivateKeyInfo(); + pkcs8.privateKeyAlgorithm.algorithm = algorithm.name.toLowerCase() === "ed25519" + ? core.asn1.idEd25519 + : core.asn1.idX25519; + pkcs8.privateKey = AsnConvert.serialize(privateData); + const raw = AsnConvert.serialize(pkcs8); + const pem = core.PemConverter.fromBufferSource(raw, "PRIVATE KEY"); + return new Ed25519PrivateKey(algorithm, extractable, keyUsages, pem); + } else if (jwk.x) { + // public key + const pubKey = crypto.createPublicKey({ + format: "jwk", + key: jwk as crypto.JsonWebKey, + }); + const pem = pubKey.export({ format: "pem", type: "spki" }) as string; + return new Ed25519PublicKey(algorithm, extractable, keyUsages, pem); + } else { + throw new core.OperationError("keyData: Cannot import JWK. 'd' or 'x' must be presented"); + } + } + case "pkcs8": { + const pem = core.PemConverter.fromBufferSource(keyData as ArrayBuffer, "PRIVATE KEY"); + return new Ed25519PrivateKey(algorithm, extractable, keyUsages, pem); + } + case "spki": { + const pem = core.PemConverter.fromBufferSource(keyData as ArrayBuffer, "PUBLIC KEY"); + return new Ed25519PublicKey(algorithm, extractable, keyUsages, pem); + } + case "raw": { + const raw = keyData as ArrayBuffer; + const key = crypto.createPublicKey({ + format: "jwk", + key: { + kty: "OKP", + crv: algorithm.name.toLowerCase() === "ed25519" ? "Ed25519" : "X25519", + x: Convert.ToBase64Url(raw), + }, + }); + const pem = key.export({ format: "pem", type: "spki" }) as string; + return new Ed25519PublicKey(algorithm, extractable, keyUsages, pem); + } + default: + return Promise.reject(new core.OperationError("format: Must be 'jwk', 'raw', pkcs8' or 'spki'")); + } + } +} diff --git a/src/mechs/ed25519/crypto_key.ts b/src/mechs/ed25519/crypto_key.ts new file mode 100644 index 0000000..4bf5261 --- /dev/null +++ b/src/mechs/ed25519/crypto_key.ts @@ -0,0 +1,21 @@ +import { CryptoKey } from "../../keys"; + +export class Ed25519CryptoKey extends CryptoKey { + + constructor(algorithm: Algorithm, extractable: boolean, usages: KeyUsage[], data: string) { + super(); + this.algorithm = algorithm; + this.extractable = extractable; + this.usages = usages; + this.data = Buffer.from(data); + } + + public toJWK(): JsonWebKey { + return { + kty: "OKP", + crv: this.algorithm.name, + key_ops: this.usages, + ext: this.extractable, + }; + } +} diff --git a/src/mechs/ed25519/ed25519.ts b/src/mechs/ed25519/ed25519.ts new file mode 100644 index 0000000..2f0af1a --- /dev/null +++ b/src/mechs/ed25519/ed25519.ts @@ -0,0 +1,37 @@ +import * as core from "webcrypto-core"; +import { Ed25519Crypto } from "./crypto"; +import { Ed25519CryptoKey } from "./crypto_key"; +import { Ed25519PrivateKey } from "./private_key"; +import { Ed25519PublicKey } from "./public_key"; +import { getCryptoKey, setCryptoKey } from "../storage"; + +export class Ed25519Provider extends core.Ed25519Provider { + public override async onGenerateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise { + const keys = await Ed25519Crypto.generateKey(algorithm, extractable, keyUsages); + return { + privateKey: setCryptoKey(keys.privateKey as Ed25519CryptoKey), + publicKey: setCryptoKey(keys.publicKey as Ed25519CryptoKey), + }; + } + + override async onSign(algorithm: Algorithm, key: Ed25519PrivateKey, data: ArrayBuffer): Promise { + const internalKey = getCryptoKey(key) as Ed25519PrivateKey; + const signature = Ed25519Crypto.sign(algorithm, internalKey, new Uint8Array(data)); + return signature; + } + + override onVerify(algorithm: Algorithm, key: Ed25519PublicKey, signature: ArrayBuffer, data: ArrayBuffer): Promise { + const internalKey = getCryptoKey(key) as Ed25519PublicKey; + return Ed25519Crypto.verify(algorithm, internalKey, new Uint8Array(signature), new Uint8Array(data)); + } + + override async onExportKey(format: KeyFormat, key: Ed25519CryptoKey): Promise { + const internalKey = getCryptoKey(key) as Ed25519CryptoKey; + return Ed25519Crypto.exportKey(format, internalKey); + } + + override async onImportKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise { + const internalKey = await Ed25519Crypto.importKey(format, keyData, algorithm, extractable, keyUsages); + return setCryptoKey(internalKey); + } +} diff --git a/src/mechs/ed25519/index.ts b/src/mechs/ed25519/index.ts new file mode 100644 index 0000000..ea53357 --- /dev/null +++ b/src/mechs/ed25519/index.ts @@ -0,0 +1,4 @@ +export * from "./ed25519"; +export * from "./x25519"; +export * from "./private_key"; +export * from "./public_key"; diff --git a/src/mechs/ed25519/private_key.ts b/src/mechs/ed25519/private_key.ts new file mode 100644 index 0000000..8639382 --- /dev/null +++ b/src/mechs/ed25519/private_key.ts @@ -0,0 +1,23 @@ +import crypto from "crypto"; +import { AsnConvert } from "@peculiar/asn1-schema"; +import * as core from "webcrypto-core"; +import { Ed25519CryptoKey } from "./crypto_key"; + +export class Ed25519PrivateKey extends Ed25519CryptoKey { + public override type = "private" as const; + + public override toJWK(): JsonWebKey { + const pubJwk = crypto.createPublicKey({ + key: this.data, + format: "pem", + }).export({ format: "jwk" }) as JsonWebKey; + const raw = core.PemConverter.toUint8Array(this.data.toString()); + const pkcs8 = AsnConvert.parse(raw, core.asn1.PrivateKeyInfo); + const d = AsnConvert.parse(pkcs8.privateKey, core.asn1.EdPrivateKey).value; + return { + ...super.toJWK(), + ...pubJwk, + d: Buffer.from(new Uint8Array(d)).toString("base64url"), + }; + } +} diff --git a/src/mechs/ed25519/public_key.ts b/src/mechs/ed25519/public_key.ts new file mode 100644 index 0000000..ca2c030 --- /dev/null +++ b/src/mechs/ed25519/public_key.ts @@ -0,0 +1,18 @@ +import crypto from "crypto"; +import { Ed25519CryptoKey } from "./crypto_key"; + +export class Ed25519PublicKey extends Ed25519CryptoKey { + public override type = "public" as const; + + public override toJWK(): JsonWebKey { + const jwk = crypto.createPublicKey({ + key: this.data, + format: "pem", + }).export({ format: "jwk" }) as JsonWebKey; + + return { + ...super.toJWK(), + ...jwk, + }; + } +} diff --git a/src/mechs/ed25519/x25519.ts b/src/mechs/ed25519/x25519.ts new file mode 100644 index 0000000..a25e778 --- /dev/null +++ b/src/mechs/ed25519/x25519.ts @@ -0,0 +1,54 @@ +import crypto from "crypto"; +import * as core from "webcrypto-core"; +import { Ed25519Crypto } from "./crypto"; +import { Ed25519CryptoKey } from "./crypto_key"; +import { CryptoKey } from "../../keys"; +import { getCryptoKey, setCryptoKey } from "../storage"; + +export class X25519Provider extends core.X25519Provider { + public override async onGenerateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise { + const keys = await Ed25519Crypto.generateKey(algorithm, extractable, keyUsages); + return { + privateKey: setCryptoKey(keys.privateKey as Ed25519CryptoKey), + publicKey: setCryptoKey(keys.publicKey as Ed25519CryptoKey), + }; + } + + public override async onDeriveBits(algorithm: EcdhKeyDeriveParams, baseKey: Ed25519CryptoKey, length: number): Promise { + const internalBaseKey = getCryptoKey(baseKey); + const internalPublicKey = getCryptoKey(algorithm.public); + const publicKey = crypto.createPublicKey({ + key: internalPublicKey.data.toString(), + format: "pem", + type: "spki", + }); + const privateKey = crypto.createPrivateKey({ + key: internalBaseKey.data.toString(), + format: "pem", + type: "pkcs8", + }); + const bits = crypto.diffieHellman({ + publicKey, + privateKey, + }); + + return new Uint8Array(bits).buffer.slice(0, length >> 3); + } + + public override async onExportKey(format: KeyFormat, key: Ed25519CryptoKey): Promise { + const internalKey = getCryptoKey(key); + return Ed25519Crypto.exportKey(format, internalKey as Ed25519CryptoKey); + } + + public override async onImportKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise { + const key = await Ed25519Crypto.importKey(format, keyData, algorithm, extractable, keyUsages); + return setCryptoKey(key); + } + + override checkCryptoKey(key: CryptoKey, keyUsage?: KeyUsage | undefined): void { + super.checkCryptoKey(key, keyUsage); + if (!(getCryptoKey(key) instanceof Ed25519CryptoKey)) { + throw new TypeError("key: Is not a Ed25519CryptoKey"); + } + } +} diff --git a/src/mechs/index.ts b/src/mechs/index.ts index 238ad11..a472234 100644 --- a/src/mechs/index.ts +++ b/src/mechs/index.ts @@ -3,6 +3,7 @@ export * from "./des"; export * from "./rsa"; export * from "./ec"; export * from "./ed"; +export * from "./ed25519"; export * from "./sha"; export * from "./pbkdf"; export * from "./hmac"; diff --git a/src/subtle.ts b/src/subtle.ts index c888258..a7dd8b3 100644 --- a/src/subtle.ts +++ b/src/subtle.ts @@ -8,6 +8,7 @@ import { EcdsaProvider, HkdfProvider, EdDsaProvider, EcdhEsProvider, + Ed25519Provider, X25519Provider, HmacProvider, Pbkdf2Provider, RsaEsProvider, RsaOaepProvider, RsaPssProvider, RsaSsaProvider, @@ -95,6 +96,9 @@ export class SubtleCrypto extends core.SubtleCrypto { //#region ECDH-ES this.providers.set(new EcdhEsProvider()); //#endregion + + this.providers.set(new Ed25519Provider()); + this.providers.set(new X25519Provider()); } } } diff --git a/test/crypto.ts b/test/crypto.ts index 7e6e3e7..b135c7e 100644 --- a/test/crypto.ts +++ b/test/crypto.ts @@ -305,4 +305,226 @@ context("Crypto", () => { assert.strictEqual((keys.privateKey.algorithm as RsaHashedKeyAlgorithm).modulusLength, 3072); }).timeout(5000); + + context("Ed25519", () => { + context("generateKey", () => { + it("should generate key pair", async () => { + const alg = { name: "ed25519" }; + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + assert.ok("privateKey" in keys); + assert.ok("publicKey" in keys); + assert.strictEqual(keys.privateKey.algorithm.name, "Ed25519"); + assert.strictEqual(keys.privateKey.type, "private"); + assert.strictEqual(keys.privateKey.extractable, false); + assert.deepStrictEqual(keys.privateKey.usages, ["sign"]); + assert.strictEqual(keys.publicKey.algorithm.name, "Ed25519"); + assert.strictEqual(keys.publicKey.type, "public"); + assert.strictEqual(keys.publicKey.extractable, true); + assert.deepStrictEqual(keys.publicKey.usages, ["verify"]); + }); + }); + context("sign/verify", () => { + it("should sign and verify data", async () => { + const alg = { name: "ed25519" }; + const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]); + assert.ok("privateKey" in keys); + const data = Buffer.from("message"); + const signature = await crypto.subtle.sign(alg, keys.privateKey, data); + assert.ok(signature instanceof ArrayBuffer); + assert.strictEqual(signature.byteLength, 64); + const ok = await crypto.subtle.verify(alg, keys.publicKey, signature, data); + assert.ok(ok); + }); + }); + context("import/export", () => { + let keys: CryptoKeyPair; + before(async () => { + const alg = { name: "ed25519" }; + keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]) as CryptoKeyPair; + }); + context("private key", () => { + it("JWK", async () => { + const jwk = await crypto.subtle.exportKey("jwk", keys.privateKey); + // { + // key_ops: [ 'sign' ], + // ext: true, + // crv: 'Ed25519', + // d: 'zAe58NtXQO0A_nNc7REZroi3CARzn31jFr80RSjcrwI', + // x: 'xONSvzKNx83TPrwuqLF6TxhlHR8aIfnVhbSAsJ2M-VI', + // kty: 'OKP' + // } + assert.strictEqual(jwk.kty, "OKP"); + assert.strictEqual(jwk.crv, "Ed25519"); + assert.ok(jwk.d); + assert.ok(jwk.x); + assert.strictEqual(jwk.key_ops?.length, 1); + assert.strictEqual(jwk.key_ops![0], "sign"); + assert.strictEqual(jwk.ext, true); + assert.strictEqual(Buffer.from(jwk.d, "base64url").byteLength, 32); + assert.strictEqual(Buffer.from(jwk.x, "base64url").byteLength, 32); + + const alg = { name: "ed25519" }; + const key = await crypto.subtle.importKey("jwk", jwk, alg, false, ["sign"]); + assert.strictEqual(key.type, "private"); + assert.strictEqual(key.extractable, false); + assert.deepStrictEqual(key.usages, ["sign"]); + assert.strictEqual(key.algorithm.name, "Ed25519"); + }); + it("PKCS8", async () => { + const alg = "ed25519"; + const pkcs8 = await crypto.subtle.exportKey("pkcs8", keys.privateKey); + const key = await crypto.subtle.importKey("pkcs8", pkcs8, alg, false, ["sign"]); + assert.strictEqual(key.type, "private"); + assert.strictEqual(key.extractable, false); + assert.deepStrictEqual(key.usages, ["sign"]); + assert.strictEqual(key.algorithm.name, "Ed25519"); + }); + }); + context("public key", () => { + it("JWK", async () => { + const jwk = await crypto.subtle.exportKey("jwk", keys.publicKey); + assert.strictEqual(jwk.kty, "OKP"); + assert.strictEqual(jwk.crv, "Ed25519"); + assert.ok(jwk.x); + assert.strictEqual(jwk.key_ops?.length, 1); + assert.strictEqual(jwk.key_ops![0], "verify"); + assert.strictEqual(jwk.ext, true); + + const alg = { name: "ed25519" }; + const key = await crypto.subtle.importKey("jwk", jwk, alg, true, ["verify"]); + assert.strictEqual(key.type, "public"); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, ["verify"]); + assert.strictEqual(key.algorithm.name, "Ed25519"); + }); + it("SPKI", async () => { + const alg = "ed25519"; + const spki = await crypto.subtle.exportKey("spki", keys.publicKey); + const key = await crypto.subtle.importKey("spki", spki, alg, true, ["verify"]); + assert.strictEqual(key.type, "public"); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, ["verify"]); + assert.strictEqual(key.algorithm.name, "Ed25519"); + }); + it("RAW", async () => { + const alg = "ed25519"; + const raw = await crypto.subtle.exportKey("raw", keys.publicKey); + const key = await crypto.subtle.importKey("raw", raw, alg, true, ["verify"]); + assert.strictEqual(key.type, "public"); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, ["verify"]); + assert.strictEqual(key.algorithm.name, "Ed25519"); + }); + }); + }); + context("Vector data", () => { + const privateKeyJwk = { + key_ops: ['sign'], + ext: true, + crv: 'Ed25519', + d: 'EOxUciT6spODKVW-JQXGcIN59oqLvkZU52g8i8bR7Wo', + x: 'Wn9sXjjBJX1BH0A_lVbR_nY8kITs06nkxFl9ZE9XgSg', + kty: 'OKP' + }; + + const publicKeyJwk = { + key_ops: ['verify'], + ext: true, + crv: 'Ed25519', + x: 'Wn9sXjjBJX1BH0A_lVbR_nY8kITs06nkxFl9ZE9XgSg', + kty: 'OKP' + }; + + context("import/export", () => { + it("should correctly import and export private key", async () => { + const alg = { name: "ed25519" }; + const importedPrivateKey = await crypto.subtle.importKey("jwk", privateKeyJwk, alg, true, ["sign"]); + const exportedPrivateKeyJwk = await crypto.subtle.exportKey("jwk", importedPrivateKey); + assert.deepStrictEqual(exportedPrivateKeyJwk, privateKeyJwk); + }); + + it("should correctly import and export public key", async () => { + const alg = { name: "ed25519" }; + const importedPublicKey = await crypto.subtle.importKey("jwk", publicKeyJwk, alg, true, ["verify"]); + const exportedPublicKeyJwk = await crypto.subtle.exportKey("jwk", importedPublicKey); + assert.deepStrictEqual(exportedPublicKeyJwk, publicKeyJwk); + }); + }); + + context("sign/verify", () => { + it("should sign and verify data", async () => { + const alg = { name: "ed25519" }; + const importedPrivateKey = await crypto.subtle.importKey("jwk", privateKeyJwk, alg, false, ["sign"]); + const importedPublicKey = await crypto.subtle.importKey("jwk", publicKeyJwk, alg, true, ["verify"]); + const data = Buffer.from("message"); + const signature = await crypto.subtle.sign(alg, importedPrivateKey, data); + const ok = await crypto.subtle.verify(alg, importedPublicKey, signature, data); + assert.ok(ok); + }); + }); + }); + }); + context("X25519", () => { + const privateKeyJwk = { + key_ops: ['deriveBits', 'deriveKey'], + ext: true, + crv: 'X25519', + d: 'AGHXWdGVQi8Is-A4uXYbfpTfDFwxGmJgCLFRHUjb0kM', + x: 'BLsolmWGd1aTexAd_O7MQnL9MpRPVKFO7t9k5Ri04lI', + kty: 'OKP' + }; + + const publicKeyJwk = { + key_ops: [], + ext: true, + crv: 'X25519', + x: 'BLsolmWGd1aTexAd_O7MQnL9MpRPVKFO7t9k5Ri04lI', + kty: 'OKP' + }; + + const derivedBitsBase64 = 'lWSsBfIyBlat6Q4vHS/MmKXN0Wraz7F82D8prcSRlHw='; + + context("generateKey", () => { + it("should generate key pair", async () => { + const alg = { name: "x25519" }; + const keys = await crypto.subtle.generateKey(alg, true, ["deriveBits", "deriveKey"]); + assert.ok("privateKey" in keys); + assert.ok("publicKey" in keys); + assert.strictEqual(keys.privateKey.algorithm.name, "X25519"); + assert.strictEqual(keys.privateKey.type, "private"); + assert.strictEqual(keys.privateKey.extractable, true); + assert.deepStrictEqual(keys.privateKey.usages, ["deriveBits", "deriveKey"]); + assert.strictEqual(keys.publicKey.algorithm.name, "X25519"); + assert.strictEqual(keys.publicKey.type, "public"); + assert.strictEqual(keys.publicKey.extractable, true); + assert.deepStrictEqual(keys.publicKey.usages, []); + }); + }); + + context("import/export", () => { + it("should correctly import and export private key", async () => { + const alg = { name: "x25519" }; + const importedPrivateKey = await crypto.subtle.importKey("jwk", privateKeyJwk, alg, true, ["deriveBits", "deriveKey"]); + const exportedPrivateKeyJwk = await crypto.subtle.exportKey("jwk", importedPrivateKey); + assert.deepStrictEqual(exportedPrivateKeyJwk, privateKeyJwk); + }); + + it("should correctly import and export public key", async () => { + const alg = { name: "x25519" }; + const importedPublicKey = await crypto.subtle.importKey("jwk", publicKeyJwk, alg, true, []); + const exportedPublicKeyJwk = await crypto.subtle.exportKey("jwk", importedPublicKey); + assert.deepStrictEqual(exportedPublicKeyJwk, publicKeyJwk); + }); + }); + + context("deriveBits", () => { + it("should derive bits", async () => { + const alg = { name: "x25519" }; + const importedPrivateKey = await crypto.subtle.importKey("jwk", privateKeyJwk, alg, false, ["deriveBits"]); + const importedPublicKey = await crypto.subtle.importKey("jwk", publicKeyJwk, alg, true, []); + const bits = await crypto.subtle.deriveBits({ name: "x25519", public: importedPublicKey } as EcdhKeyDeriveParams, importedPrivateKey, 256); + assert.strictEqual(Buffer.from(bits).toString("base64"), derivedBitsBase64); + }); + }); + }); });