From 590a5fcedf257a144ce60e7338d19639b9f25c68 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Tue, 28 Nov 2023 17:24:17 -0500 Subject: [PATCH] Refactor `@web5/crypto` to replace Web Crypto `CryptoKey` with JWK (#318) * Refactor Ed25519 to generateKey instead of generateKeyPair * Refactor Secp256k1 to generateKey instead of generateKeyPair and simplify sign/verify * Refactor X25519 to generateKey instead of generateKeyPair * Refactor PBKDF2 to use JWKs * Remove CryptoKeyToJwkMixin * Improve test coverage for PBKDF2 * Refactor Ed25519, Secp256k1, and X25519 to use JWKs * Refactor EcdhAlgorithm to use JWK * Refactor EcdsaAlgorithm to use JWK * Refactor EdDsaAlgorithm to use JWK * Refactor AesCtrAlgorithm to use JWK * Refactor AesCtrAlgorithm to JWK * Refactor AesGcm to use JWK * Bump @noble ciphers, curves, and hashes dependencies --------- Signed-off-by: Frank Hinek --- .web5-spec/credentials.ts | 7 +- package-lock.json | 531 ++++-- packages/crypto/package.json | 8 +- .../crypto/src/algorithms-api/aes/base.ts | 59 +- packages/crypto/src/algorithms-api/aes/ctr.ts | 73 +- .../src/algorithms-api/crypto-algorithm.ts | 77 +- .../crypto/src/algorithms-api/crypto-key.ts | 56 - packages/crypto/src/algorithms-api/ec/base.ts | 113 +- packages/crypto/src/algorithms-api/ec/ecdh.ts | 67 +- .../crypto/src/algorithms-api/ec/ecdsa.ts | 53 +- .../crypto/src/algorithms-api/ec/eddsa.ts | 48 +- packages/crypto/src/algorithms-api/index.ts | 1 - .../crypto/src/algorithms-api/pbkdf/pbkdf2.ts | 55 +- .../crypto/src/crypto-algorithms/aes-ctr.ts | 60 +- packages/crypto/src/crypto-algorithms/ecdh.ts | 94 +- .../crypto/src/crypto-algorithms/ecdsa.ts | 99 +- .../crypto/src/crypto-algorithms/eddsa.ts | 96 +- .../crypto/src/crypto-algorithms/pbkdf2.ts | 41 +- .../crypto/src/crypto-primitives/aes-ctr.ts | 252 ++- .../crypto/src/crypto-primitives/aes-gcm.ts | 267 ++- .../crypto/src/crypto-primitives/ed25519.ts | 511 ++++-- .../crypto/src/crypto-primitives/pbkdf2.ts | 62 + .../crypto/src/crypto-primitives/secp256k1.ts | 789 ++++++--- .../crypto/src/crypto-primitives/x25519.ts | 375 ++++- .../crypto-primitives/xchacha20-poly1305.ts | 248 ++- .../crypto/src/crypto-primitives/xchacha20.ts | 232 ++- packages/crypto/src/index.ts | 1 - packages/crypto/src/jose.ts | 511 ++---- packages/crypto/src/types/crypto-key.ts | 4 - packages/crypto/src/types/web5-crypto.ts | 74 +- packages/crypto/src/utils.ts | 32 +- packages/crypto/tests/algorithms-api.spec.ts | 1299 +++++++++------ .../crypto/tests/crypto-algorithms.spec.ts | 1460 ++++++----------- .../crypto/tests/crypto-primitives.spec.ts | 1272 -------------- .../tests/crypto-primitives/aes-ctr.spec.ts | 153 ++ .../tests/crypto-primitives/aes-gcm.spec.ts | 156 ++ .../crypto-primitives/concat-kdf.spec.ts | 129 ++ .../tests/crypto-primitives/ed25519.spec.ts | 381 +++++ .../tests/crypto-primitives/pbkdf2.spec.ts | 133 ++ .../tests/crypto-primitives/secp256k1.spec.ts | 442 +++++ .../tests/crypto-primitives/x25519.spec.ts | 241 +++ .../xchacha20-poly1305.spec.ts | 190 +++ .../tests/crypto-primitives/xchacha20.spec.ts | 153 ++ .../tests/fixtures/test-vectors/ed25519.ts | 20 - .../ed25519/bytes-to-private-key.json | 44 + .../ed25519/bytes-to-public-key.json | 41 + .../ed25519/compute-public-key.json | 73 + .../convert-private-key-to-x25519.json | 41 + .../ed25519/convert-public-key-to-x25519.json | 37 + .../ed25519/private-key-to-bytes.json | 44 + .../ed25519/public-key-to-bytes.json | 41 + .../fixtures/test-vectors/ed25519/sign.json | 61 + .../fixtures/test-vectors/ed25519/verify.json | 61 + .../tests/fixtures/test-vectors/jose.ts | 632 +------ .../tests/fixtures/test-vectors/secp256k1.ts | 46 - .../secp256k1/bytes-to-private-key.json | 61 + .../secp256k1/bytes-to-public-key.json | 57 + .../secp256k1/get-curve-points.json | 45 + .../secp256k1/private-key-to-bytes.json | 61 + .../secp256k1/public-key-to-bytes.json | 57 + .../secp256k1/validate-private-key.json | 33 + .../secp256k1/validate-public-key.json | 33 + .../x25519/bytes-to-private-key.json | 44 + .../x25519/bytes-to-public-key.json | 41 + .../x25519/private-key-to-bytes.json | 44 + .../x25519/public-key-to-bytes.json | 41 + packages/crypto/tests/jose.spec.ts | 648 ++++---- packages/crypto/tests/tsconfig.json | 3 +- packages/crypto/tests/utils.spec.ts | 38 - packages/identity-agent/package.json | 4 +- 70 files changed, 8001 insertions(+), 5255 deletions(-) delete mode 100644 packages/crypto/src/algorithms-api/crypto-key.ts delete mode 100644 packages/crypto/src/types/crypto-key.ts delete mode 100644 packages/crypto/tests/crypto-primitives.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/aes-ctr.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/aes-gcm.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/concat-kdf.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/ed25519.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/pbkdf2.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/secp256k1.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/x25519.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/xchacha20-poly1305.spec.ts create mode 100644 packages/crypto/tests/crypto-primitives/xchacha20.spec.ts delete mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519.ts create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-private-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-public-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/compute-public-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/convert-private-key-to-x25519.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/convert-public-key-to-x25519.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/private-key-to-bytes.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/public-key-to-bytes.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/sign.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/ed25519/verify.json delete mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1.ts create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-private-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/get-curve-points.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/private-key-to-bytes.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/public-key-to-bytes.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-public-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-private-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-public-key.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/x25519/private-key-to-bytes.json create mode 100644 packages/crypto/tests/fixtures/test-vectors/x25519/public-key-to-bytes.json diff --git a/.web5-spec/credentials.ts b/.web5-spec/credentials.ts index 04449ce01..c2a2787d3 100644 --- a/.web5-spec/credentials.ts +++ b/.web5-spec/credentials.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { VerifiableCredential, SignOptions } from '@web5/credentials'; import { DidKeyMethod, PortableDid } from '@web5/dids'; -import { Ed25519, Jose } from '@web5/crypto'; +import { Ed25519, PrivateKeyJwk } from '@web5/crypto'; import { paths } from './openapi.js'; type Signer = (data: Uint8Array) => Promise; @@ -24,9 +24,8 @@ export async function credentialIssue(req: Request, res: Response) { // build signing options const [signingKeyPair] = ownDid.keySet.verificationMethodKeys!; - const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; const subjectIssuerDid = body.credential.credentialSubject["id"] as string; - const signer = EdDsaSigner(privateKey); + const signer = EdDsaSigner(signingKeyPair.privateKeyJwk as PrivateKeyJwk); const signOptions: SignOptions = { issuerDid : ownDid.did, subjectDid : subjectIssuerDid, @@ -51,7 +50,7 @@ export async function credentialIssue(req: Request, res: Response) { res.json(resp); } -function EdDsaSigner(privateKey: Uint8Array): Signer { +function EdDsaSigner(privateKey: PrivateKeyJwk): Signer { return async (data: Uint8Array): Promise => { const signature = await Ed25519.sign({ data, key: privateKey}); return signature; diff --git a/package-lock.json b/package-lock.json index 2e72351ce..c663b537f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -757,19 +757,19 @@ } }, "node_modules/@noble/ciphers": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", - "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.4.0.tgz", + "integrity": "sha512-xaUaUUDWbHIFSxaQ/pIe+33VG2mfJp6N/KxKLmZr5biWdNznCAmfu24QRhX10BbVAuqOahAoyp0S4M9md6GPDw==", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dependencies": { - "@noble/hashes": "1.3.1" + "@noble/hashes": "1.3.2" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -787,9 +787,9 @@ ] }, "node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "engines": { "node": ">= 16" }, @@ -998,9 +998,9 @@ } }, "node_modules/@sphereon/pex-models": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.1.1.tgz", - "integrity": "sha512-0UX/CMwgiJSxzuBn6SLOTSKkm+uPq3dkNjl8w4EtppXp6zBB4lQMd1mJX7OifX5Bp5vPUfoz7bj2B+yyDtbZww==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@sphereon/pex-models/-/pex-models-2.1.2.tgz", + "integrity": "sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w==" }, "node_modules/@sphereon/ssi-types": { "version": "0.13.0", @@ -1109,9 +1109,9 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", - "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { "@types/node": "*" @@ -1187,9 +1187,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", - "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1206,9 +1206,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/sinon": { @@ -1268,16 +1268,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", - "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { @@ -1297,14 +1297,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", - "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1315,13 +1315,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1430,9 +1430,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", - "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "peer": true, "engines": { @@ -1444,14 +1444,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", - "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1472,13 +1472,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1806,6 +1806,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, "node_modules/abort-controller": { @@ -2619,9 +2620,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001563", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz", - "integrity": "sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw==", + "version": "1.0.30001565", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", + "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", "dev": true, "funding": [ { @@ -3289,14 +3290,6 @@ "uint8arrays": "3.1.1" } }, - "node_modules/did-jwt/node_modules/@noble/ciphers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.4.0.tgz", - "integrity": "sha512-xaUaUUDWbHIFSxaQ/pIe+33VG2mfJp6N/KxKLmZr5biWdNznCAmfu24QRhX10BbVAuqOahAoyp0S4M9md6GPDw==", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/did-jwt/node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -3419,28 +3412,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/eciesjs/node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/eciesjs/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3448,9 +3419,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.588", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.588.tgz", - "integrity": "sha512-soytjxwbgcCu7nh5Pf4S2/4wa6UIu+A3p03U2yVr53qGxi1/VTR3ENI+p50v+UxqqZAfl48j3z55ud7VHIOr9w==", + "version": "1.4.595", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.595.tgz", + "integrity": "sha512-+ozvXuamBhDOKvMNUQvecxfbyICmIAwS4GpLmR0bsiSBlGnLaOcs2Cj7J8XSbW+YEaN3Xl3ffgpm+srTUWFwFQ==", "dev": true, "peer": true }, @@ -4584,9 +4555,9 @@ } }, "node_modules/hamt-sharding/node_modules/uint8arrays": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz", - "integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.9.tgz", + "integrity": "sha512-iHU8XJJnfeijILZWzV7RgILdPHqe0mjJvyzY4mO8aUUtHsDbPa2Gc8/02Kc4zeokp2W6Qq8z9Ap1xkQ1HfbKwg==", "dependencies": { "multiformats": "^12.0.1" } @@ -4970,9 +4941,9 @@ } }, "node_modules/ipfs-unixfs-exporter/node_modules/uint8arrays": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz", - "integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.9.tgz", + "integrity": "sha512-iHU8XJJnfeijILZWzV7RgILdPHqe0mjJvyzY4mO8aUUtHsDbPa2Gc8/02Kc4zeokp2W6Qq8z9Ap1xkQ1HfbKwg==", "dependencies": { "multiformats": "^12.0.1" } @@ -5014,9 +4985,9 @@ } }, "node_modules/ipfs-unixfs-importer/node_modules/uint8arrays": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz", - "integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.9.tgz", + "integrity": "sha512-iHU8XJJnfeijILZWzV7RgILdPHqe0mjJvyzY4mO8aUUtHsDbPa2Gc8/02Kc4zeokp2W6Qq8z9Ap1xkQ1HfbKwg==", "dependencies": { "multiformats": "^12.0.1" } @@ -5640,9 +5611,9 @@ "dev": true }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -6302,9 +6273,9 @@ } }, "node_modules/lru-cache": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz", - "integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -6843,9 +6814,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.0.tgz", - "integrity": "sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz", + "integrity": "sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -7488,9 +7459,9 @@ } }, "node_modules/protons-runtime/node_modules/uint8arrays": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz", - "integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.9.tgz", + "integrity": "sha512-iHU8XJJnfeijILZWzV7RgILdPHqe0mjJvyzY4mO8aUUtHsDbPa2Gc8/02Kc4zeokp2W6Qq8z9Ap1xkQ1HfbKwg==", "dependencies": { "multiformats": "^12.0.1" } @@ -9112,15 +9083,11 @@ } }, "node_modules/uint8arraylist": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.3.tgz", - "integrity": "sha512-oEVZr4/GrH87K0kjNce6z8pSCzLEPqHNLNR5sj8cJOySrTP8Vb/pMIbZKLJGhQKxm1TiZ31atNrpn820Pyqpow==", + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.7.tgz", + "integrity": "sha512-ohRElqR6C5dd60vRFLq40MCiSnUe1AzkpHvbCEMCGGP6zMoFYECsjdhL6bR1kTK37ONNRDuHQ3RIpScRYcYYIg==", "dependencies": { "uint8arrays": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" } }, "node_modules/uint8arraylist/node_modules/multiformats": { @@ -9133,9 +9100,9 @@ } }, "node_modules/uint8arraylist/node_modules/uint8arrays": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz", - "integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.9.tgz", + "integrity": "sha512-iHU8XJJnfeijILZWzV7RgILdPHqe0mjJvyzY4mO8aUUtHsDbPa2Gc8/02Kc4zeokp2W6Qq8z9Ap1xkQ1HfbKwg==", "dependencies": { "multiformats": "^12.0.1" } @@ -9314,9 +9281,9 @@ } }, "node_modules/v8-to-istanbul": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", - "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -9835,6 +9802,36 @@ "node": ">=18.0.0" } }, + "packages/agent/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/agent/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/agent/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/agent/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -9903,6 +9900,20 @@ } } }, + "packages/agent/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10031,6 +10042,36 @@ "node": ">=18.0.0" } }, + "packages/api/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/api/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/api/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/api/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -10099,6 +10140,20 @@ } } }, + "packages/api/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/api/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10399,6 +10454,39 @@ "node": ">=18.0.0" } }, + "packages/credentials/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "dev": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/credentials/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/credentials/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "dev": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/credentials/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -10467,6 +10555,21 @@ } } }, + "packages/credentials/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dev": true, + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/credentials/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10545,12 +10648,12 @@ }, "packages/crypto": { "name": "@web5/crypto", - "version": "0.2.2", + "version": "0.2.3", "license": "Apache-2.0", "dependencies": { - "@noble/ciphers": "0.1.4", - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", + "@noble/ciphers": "0.4.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", "@web5/common": "0.2.1" }, "devDependencies": { @@ -10784,6 +10887,36 @@ "node": ">=18.0.0" } }, + "packages/dids/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/dids/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/dids/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/dids/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -10852,6 +10985,20 @@ } } }, + "packages/dids/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/dids/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10934,7 +11081,9 @@ "license": "Apache-2.0", "dependencies": { "@web5/agent": "0.2.4", - "@web5/api": "0.8.3" + "@web5/common": "0.2.1", + "@web5/crypto": "0.2.2", + "@web5/dids": "0.2.2" }, "devDependencies": { "@playwright/test": "1.36.2", @@ -10968,6 +11117,36 @@ "node": ">=18.0.0" } }, + "packages/identity-agent/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/identity-agent/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/identity-agent/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/identity-agent/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -11036,6 +11215,20 @@ } } }, + "packages/identity-agent/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/identity-agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -11154,6 +11347,36 @@ "node": ">=18.0.0" } }, + "packages/proxy-agent/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/proxy-agent/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/proxy-agent/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/proxy-agent/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -11222,6 +11445,20 @@ } } }, + "packages/proxy-agent/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/proxy-agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -11340,6 +11577,36 @@ "node": ">=18.0.0" } }, + "packages/user-agent/node_modules/@noble/ciphers": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.1.4.tgz", + "integrity": "sha512-d3ZR8vGSpy3v/nllS+bD/OMN5UZqusWiQqkyj7AwzTnhXFH72pF5oB4Ach6DQ50g5kXxC28LdaYBEpsyv9KOUQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/user-agent/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/user-agent/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "packages/user-agent/node_modules/@typescript-eslint/parser": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.4.0.tgz", @@ -11408,6 +11675,20 @@ } } }, + "packages/user-agent/node_modules/@web5/crypto": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@web5/crypto/-/crypto-0.2.2.tgz", + "integrity": "sha512-vHFg0wXQSQXrwuBNQyDHnmSZchfTfO6/Sv+7rDsNkvofs+6lGTE8CZ02cwUYMeIwTRMLer12c+fMfzYrXokEUQ==", + "dependencies": { + "@noble/ciphers": "0.1.4", + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@web5/common": "0.2.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/user-agent/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 610db602b..70f35d6a5 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@web5/crypto", - "version": "0.2.2", + "version": "0.2.3", "description": "TBD crypto library", "type": "module", "main": "./dist/cjs/index.js", @@ -73,9 +73,9 @@ "node": ">=18.0.0" }, "dependencies": { - "@noble/ciphers": "0.1.4", - "@noble/curves": "1.1.0", - "@noble/hashes": "1.3.1", + "@noble/ciphers": "0.4.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", "@web5/common": "0.2.1" }, "devDependencies": { diff --git a/packages/crypto/src/algorithms-api/aes/base.ts b/packages/crypto/src/algorithms-api/aes/base.ts index ad29f93ea..2158ab433 100644 --- a/packages/crypto/src/algorithms-api/aes/base.ts +++ b/packages/crypto/src/algorithms-api/aes/base.ts @@ -1,49 +1,64 @@ -import { universalTypeOf } from '@web5/common'; - import type { Web5Crypto } from '../../types/web5-crypto.js'; +import type { JwkOperation, PrivateKeyJwk } from '../../jose.js'; +import { Jose } from '../../jose.js'; +import { InvalidAccessError } from '../errors.js'; import { checkRequiredProperty } from '../../utils.js'; import { CryptoAlgorithm } from '../crypto-algorithm.js'; -import { InvalidAccessError, OperationError } from '../errors.js'; export abstract class BaseAesAlgorithm extends CryptoAlgorithm { - public checkGenerateKey(options: { + public checkGenerateKeyOptions(options: { algorithm: Web5Crypto.AesGenerateKeyOptions, - keyUsages: Web5Crypto.KeyUsage[] + keyOperations: JwkOperation[] }): void { - const { algorithm, keyUsages } = options; + const { algorithm, keyOperations } = options; + // Algorithm specified in the operation must match the algorithm implementation processing the operation. this.checkAlgorithmName({ algorithmName: algorithm.name }); - // The algorithm object must contain a length property. - checkRequiredProperty({ property: 'length', inObject: algorithm }); - // The length specified must be a number. - if (universalTypeOf(algorithm.length) !== 'Number') { - throw new TypeError(`Algorithm 'length' is not of type: Number.`); + + // If specified, key operations must be permitted by the algorithm implementation processing the operation. + if (keyOperations) { + this.checkKeyOperations({ keyOperations, allowedKeyOperations: this.keyOperations }); } - // The length specified must be one of the allowed bit lengths for AES. - if (![128, 192, 256].includes(algorithm.length)) { - throw new OperationError(`Algorithm 'length' must be 128, 192, or 256.`); + } + + public checkSecretKey(options: { + key: PrivateKeyJwk + }): void { + const { key } = options; + + // The options object must contain a key property. + checkRequiredProperty({ property: 'key', inObject: options }); + + // The key object must be a JSON Web key (JWK). + this.checkJwk({ key }); + + // The key object must be an octet sequence (oct) private key in JWK format. + if (!Jose.isOctPrivateKeyJwk(key)) { + throw new InvalidAccessError('Requested operation is only valid for oct private keys.'); + } + + // If specified, the key's algorithm must match the algorithm implementation processing the operation. + if (key.alg) { + this.checkKeyAlgorithm({ keyAlgorithmName: key.alg }); } - // The key usages specified must be permitted by the algorithm implementation processing the operation. - this.checkKeyUsages({ keyUsages, allowedKeyUsages: this.keyUsages }); } public abstract generateKey(options: { algorithm: Web5Crypto.AesGenerateKeyOptions, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise; + keyOperations: JwkOperation[] + }): Promise; public override async deriveBits(): Promise { - throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for AES algorithm.`); } public override async sign(): Promise { - throw new InvalidAccessError(`Requested operation 'sign' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'sign' is not valid for AES algorithm.`); } public override async verify(): Promise { - throw new InvalidAccessError(`Requested operation 'verify' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'verify' is not valid for AES algorithm.`); } } \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/aes/ctr.ts b/packages/crypto/src/algorithms-api/aes/ctr.ts index 18b2c958b..bc619de40 100644 --- a/packages/crypto/src/algorithms-api/aes/ctr.ts +++ b/packages/crypto/src/algorithms-api/aes/ctr.ts @@ -1,6 +1,7 @@ import { universalTypeOf } from '@web5/common'; import type { Web5Crypto } from '../../types/web5-crypto.js'; +import type { JwkOperation, PrivateKeyJwk } from '../../jose.js'; import { BaseAesAlgorithm } from './base.js'; import { OperationError } from '../errors.js'; @@ -8,44 +9,88 @@ import { checkRequiredProperty } from '../../utils.js'; export abstract class BaseAesCtrAlgorithm extends BaseAesAlgorithm { - public readonly name = 'AES-CTR'; - - public readonly keyUsages: Web5Crypto.KeyUsage[] = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; + public readonly keyOperations: JwkOperation[] = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; public checkAlgorithmOptions(options: { - algorithm: Web5Crypto.AesCtrOptions, - key: Web5Crypto.CryptoKey + algorithm: Web5Crypto.AesCtrOptions }): void { - const { algorithm, key } = options; + const { algorithm } = options; + // Algorithm specified in the operation must match the algorithm implementation processing the operation. this.checkAlgorithmName({ algorithmName: algorithm.name }); + // The algorithm object must contain a counter property. checkRequiredProperty({ property: 'counter', inObject: algorithm }); + // The counter must a Uint8Array. if (!(universalTypeOf(algorithm.counter) === 'Uint8Array')) { throw new TypeError(`Algorithm 'counter' is not of type: Uint8Array.`); } + // The initial value of the counter block must be 16 bytes long (the AES block size). if (algorithm.counter.byteLength !== 16) { throw new OperationError(`Algorithm 'counter' must have length: 16 bytes.`); } + // The algorithm object must contain a length property. checkRequiredProperty({ property: 'length', inObject: algorithm }); + // The length specified must be a number. if (universalTypeOf(algorithm.length) !== 'Number') { throw new TypeError(`Algorithm 'length' is not of type: Number.`); } + // The length specified must be between 1 and 128. if ((algorithm.length < 1 || algorithm.length > 128)) { throw new OperationError(`Algorithm 'length' should be in the range: 1 to 128.`); } - // The options object must contain a key property. - checkRequiredProperty({ property: 'key', inObject: options }); - // The key object must be a CryptoKey. - this.checkCryptoKey({ key }); - // The key algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name }); - // The CryptoKey object must be a secret key. - this.checkKeyType({ keyType: key.type, allowedKeyType: 'secret' }); + } + + public checkDecryptOptions(options: { + algorithm: Web5Crypto.AesCtrOptions, + key: PrivateKeyJwk, + data: Uint8Array + }): void { + const { algorithm, key, data } = options; + + // Validate the algorithm input parameters. + this.checkAlgorithmOptions({ algorithm }); + + // Validate the secret key. + this.checkSecretKey({ key }); + + // If specified, the secret key must be allowed to be used for 'decrypt' operations. + if (key.key_ops) { + this.checkKeyOperations({ keyOperations: ['decrypt'], allowedKeyOperations: key.key_ops }); + } + + // The data must be a Uint8Array. + if (universalTypeOf(data) !== 'Uint8Array') { + throw new TypeError('The data must be of type Uint8Array.'); + } + } + + public checkEncryptOptions(options: { + algorithm: Web5Crypto.AesCtrOptions, + key: PrivateKeyJwk, + data: Uint8Array + }): void { + const { algorithm, key, data } = options; + + // Validate the algorithm and key input parameters. + this.checkAlgorithmOptions({ algorithm }); + + // Validate the secret key. + this.checkSecretKey({ key }); + + // If specified, the secret key must be allowed to be used for 'encrypt' operations. + if (key.key_ops) { + this.checkKeyOperations({ keyOperations: ['encrypt'], allowedKeyOperations: key.key_ops }); + } + + // The data must be a Uint8Array. + if (universalTypeOf(data) !== 'Uint8Array') { + throw new TypeError('The data must be of type Uint8Array.'); + } } } \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/crypto-algorithm.ts b/packages/crypto/src/algorithms-api/crypto-algorithm.ts index 7f962aaf9..59faa5a12 100644 --- a/packages/crypto/src/algorithms-api/crypto-algorithm.ts +++ b/packages/crypto/src/algorithms-api/crypto-algorithm.ts @@ -1,18 +1,19 @@ import type { Web5Crypto } from '../types/web5-crypto.js'; +import type { JsonWebKey, JwkOperation, JwkType, PrivateKeyJwk, PublicKeyJwk } from '../jose.js'; import { InvalidAccessError, NotSupportedError } from './errors.js'; export abstract class CryptoAlgorithm { /** - * Name of the algorithm + * Name(s) of the algorithm supported by the implementation. */ - public abstract readonly name: string; + public abstract readonly names: ReadonlyArray; /** * Indicates which cryptographic operations are permissible to be used with this algorithm. */ - public abstract readonly keyUsages: Web5Crypto.KeyUsage[] | Web5Crypto.KeyPairUsage; + public abstract readonly keyOperations: JwkOperation[]; public checkAlgorithmName(options: { algorithmName: string @@ -21,17 +22,17 @@ export abstract class CryptoAlgorithm { if (algorithmName === undefined) { throw new TypeError(`Required parameter missing: 'algorithmName'`); } - if (algorithmName !== this.name) { + if (!this.names.includes(algorithmName)) { throw new NotSupportedError(`Algorithm not supported: '${algorithmName}'`); } } - public checkCryptoKey(options: { - key: Web5Crypto.CryptoKey + public checkJwk(options: { + key: JsonWebKey }): void { const { key } = options; - if (!('algorithm' in key && 'extractable' in key && 'type' in key && 'usages' in key)) { - throw new TypeError('Object is not a CryptoKey'); + if (typeof key !== 'object' || !('kty' in key)) { + throw new TypeError('Object is not a JSON Web Key (JWK)'); } } @@ -42,35 +43,40 @@ export abstract class CryptoAlgorithm { if (keyAlgorithmName === undefined) { throw new TypeError(`Required parameter missing: 'keyAlgorithmName'`); } - if (keyAlgorithmName && keyAlgorithmName !== this.name) { - throw new InvalidAccessError(`Algorithm '${this.name}' does not match the provided '${keyAlgorithmName}' key.`); + if (keyAlgorithmName && !this.names.includes(keyAlgorithmName)) { + throw new InvalidAccessError(`Algorithm '${this.names.join(', ')}' does not match the provided '${keyAlgorithmName}' key.`); } } public checkKeyType(options: { - keyType: Web5Crypto.KeyType, - allowedKeyType: Web5Crypto.KeyType + keyType: JwkType, + allowedKeyTypes: JwkType[] }): void { - const { keyType, allowedKeyType } = options; - if (keyType === undefined || allowedKeyType === undefined) { - throw new TypeError(`One or more required parameters missing: 'keyType, allowedKeyType'`); + const { keyType, allowedKeyTypes } = options; + if (keyType === undefined || allowedKeyTypes === undefined) { + throw new TypeError(`One or more required parameters missing: 'keyType, allowedKeyTypes'`); } - if (keyType && keyType !== allowedKeyType) { - throw new InvalidAccessError(`Requested operation is not valid for the provided '${keyType}' key.`); + if (!Array.isArray(allowedKeyTypes)) { + throw new TypeError(`The provided 'allowedKeyTypes' is not of type Array.`); + } + if (keyType && !allowedKeyTypes.includes(keyType)) { + throw new InvalidAccessError(`Key type of the provided key must be '${allowedKeyTypes.join(', ')}' but '${keyType}' was specified.`); } } - public checkKeyUsages(options: { - keyUsages: Web5Crypto.KeyUsage[], - allowedKeyUsages: Web5Crypto.KeyUsage[] | Web5Crypto.KeyPairUsage + public checkKeyOperations(options: { + keyOperations: JwkOperation[], + allowedKeyOperations: JwkOperation[] }): void { - const { keyUsages, allowedKeyUsages } = options; - if (!(keyUsages && keyUsages.length > 0)) { - throw new TypeError(`Required parameter missing or empty: 'keyUsages'`); + const { keyOperations, allowedKeyOperations } = options; + if (!(keyOperations && keyOperations.length > 0)) { + throw new TypeError(`Required parameter missing or empty: 'keyOperations'`); + } + if (!Array.isArray(allowedKeyOperations)) { + throw new TypeError(`The provided 'allowedKeyOperations' is not of type Array.`); } - const allowedUsages = (Array.isArray(allowedKeyUsages)) ? allowedKeyUsages : [...allowedKeyUsages.privateKey, ...allowedKeyUsages.publicKey]; - if (!keyUsages.every(usage => allowedUsages.includes(usage))) { - throw new InvalidAccessError(`Requested operation(s) '${keyUsages.join(', ')}' is not valid for the provided key.`); + if (!keyOperations.every(operation => allowedKeyOperations.includes(operation))) { + throw new InvalidAccessError(`Requested operation(s) '${keyOperations.join(', ')}' is not valid for the provided key.`); } } @@ -90,37 +96,36 @@ export abstract class CryptoAlgorithm { public abstract decrypt(options: { algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.AesCtrOptions | Web5Crypto.AesGcmOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise; public abstract deriveBits(options: { - algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.EcdhDeriveKeyOptions, - baseKey: Web5Crypto.CryptoKey, - length: number | null + algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.EcdhDeriveKeyOptions | Web5Crypto.Pbkdf2Options, + baseKey: JsonWebKey, + length?: number }): Promise; public abstract encrypt(options: { algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.AesCtrOptions | Web5Crypto.AesGcmOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise; public abstract generateKey(options: { algorithm: Partial, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[], - }): Promise; + keyOperations?: JwkOperation[], + }): Promise; public abstract sign(options: { algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.EcdsaOptions | Web5Crypto.EdDsaOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise; public abstract verify(options: { algorithm: Web5Crypto.AlgorithmIdentifier | Web5Crypto.EcdsaOptions | Web5Crypto.EdDsaOptions, - key: Web5Crypto.CryptoKey, + key: PublicKeyJwk, signature: Uint8Array, data: Uint8Array }): Promise; diff --git a/packages/crypto/src/algorithms-api/crypto-key.ts b/packages/crypto/src/algorithms-api/crypto-key.ts deleted file mode 100644 index fdc05a2c3..000000000 --- a/packages/crypto/src/algorithms-api/crypto-key.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Web5Crypto } from '../types/web5-crypto.js'; - -export class CryptoKey implements Web5Crypto.CryptoKey { - public algorithm: Web5Crypto.KeyAlgorithm | Web5Crypto.GenerateKeyOptions; - public extractable: boolean; - public material: Uint8Array; - public type: Web5Crypto.KeyType; - public usages: Web5Crypto.KeyUsage[]; - - constructor (algorithm: Web5Crypto.Algorithm | Web5Crypto.GenerateKeyOptions, extractable: boolean, material: Uint8Array, type: Web5Crypto.KeyType, usages: Web5Crypto.KeyUsage[]) { - this.algorithm = algorithm; - this.extractable = extractable; - this.material = material; - this.type = type; - this.usages = usages; - - // ensure values are not writeable - Object.defineProperties(this, { - // TODO - // These properties can't be fixed immediately on creation of the - // object because the implementation may build it up in stages. - // At some point in the operations before returning a key we should - // freeze the object to prevent further manipulation. - - type: { - enumerable : true, - writable : false, - value : type - }, - extractable: { - enumerable : true, - writable : true, - value : extractable - }, - algorithm: { - enumerable : true, - writable : false, - value : algorithm - }, - usages: { - enumerable : true, - writable : true, - value : usages - }, - - // this is the "key material" used internally - // it is not enumerable, but we need it to be - // accessible by algorithm implementations - material: { - enumerable : false, - writable : false, - value : material - } - }); - } -} \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/ec/base.ts b/packages/crypto/src/algorithms-api/ec/base.ts index de47f285d..0994b9d05 100644 --- a/packages/crypto/src/algorithms-api/ec/base.ts +++ b/packages/crypto/src/algorithms-api/ec/base.ts @@ -1,39 +1,122 @@ +import { universalTypeOf } from '@web5/common'; + import type { Web5Crypto } from '../../types/web5-crypto.js'; +import type { JwkOperation, PrivateKeyJwk, PublicKeyJwk } from '../../jose.js'; +import { Jose } from '../../jose.js'; import { InvalidAccessError } from '../errors.js'; import { CryptoAlgorithm } from '../crypto-algorithm.js'; import { checkValidProperty, checkRequiredProperty } from '../../utils.js'; export abstract class BaseEllipticCurveAlgorithm extends CryptoAlgorithm { - public abstract namedCurves: string[]; + public abstract readonly curves: ReadonlyArray; - public checkGenerateKey(options: { + public checkGenerateKeyOptions(options: { algorithm: Web5Crypto.EcGenerateKeyOptions, - keyUsages: Web5Crypto.KeyUsage[] + keyOperations?: JwkOperation[] + }): void { + const { algorithm, keyOperations } = options; + + // Algorithm specified in the operation must match the algorithm implementation processing the operation. + this.checkAlgorithmName({ algorithmName: algorithm.name }); + + // The algorithm object must contain a curve property. + checkRequiredProperty({ property: 'curve', inObject: algorithm }); + + // The curve specified must be supported by the algorithm implementation processing the operation. + checkValidProperty({ property: algorithm.curve, allowedProperties: this.curves }); + + // If specified, key operations must be permitted by the algorithm implementation processing the operation. + if (keyOperations) { + this.checkKeyOperations({ keyOperations, allowedKeyOperations: this.keyOperations }); + } + } + + public checkSignOptions(options: { + algorithm: Web5Crypto.EcdsaOptions, + key: PrivateKeyJwk, + data: Uint8Array + }): void { + const { algorithm, data, key } = options; + + // Algorithm specified in the operation must match the algorithm implementation processing the operation. + this.checkAlgorithmName({ algorithmName: algorithm.name }); + + // The key object must be an Elliptic Curve (EC) or Octet Key Pair (OKP) private key in JWK format. + if (!(Jose.isEcPrivateKeyJwk(key) || Jose.isOkpPrivateKeyJwk(key))) { + throw new InvalidAccessError('Requested operation is only valid for private keys.'); + } + + // The curve specified must be supported by the algorithm implementation processing the operation. + checkValidProperty({ property: key.crv, allowedProperties: this.curves }); + + // The data must be a Uint8Array. + if (universalTypeOf(data) !== 'Uint8Array') { + throw new TypeError('The data must be of type Uint8Array.'); + } + + // If specified, the key's algorithm must match the algorithm implementation processing the operation. + if (key.alg) { + this.checkKeyAlgorithm({ keyAlgorithmName: key.alg }); + } + + // If specified, the key's `key_ops` must include the 'sign' operation. + if (key.key_ops) { + this.checkKeyOperations({ keyOperations: ['sign'], allowedKeyOperations: key.key_ops }); + } + } + + public checkVerifyOptions(options: { + algorithm: Web5Crypto.EcdsaOptions; + key: PublicKeyJwk; + signature: Uint8Array; + data: Uint8Array; }): void { - const { algorithm, keyUsages } = options; + const { algorithm, key, signature, data } = options; + // Algorithm specified in the operation must match the algorithm implementation processing the operation. this.checkAlgorithmName({ algorithmName: algorithm.name }); - // The algorithm object must contain a namedCurve property. - checkRequiredProperty({ property: 'namedCurve', inObject: algorithm }); - // The named curve specified must be supported by the algorithm implementation processing the operation. - checkValidProperty({ property: algorithm.namedCurve, allowedProperties: this.namedCurves }); - // The key usages specified must be permitted by the algorithm implementation processing the operation. - this.checkKeyUsages({ keyUsages, allowedKeyUsages: this.keyUsages }); + + // The key object must be an Elliptic Curve (EC) or Octet Key Pair (OKP) public key in JWK format. + if (!(Jose.isEcPublicKeyJwk(key) || Jose.isOkpPublicKeyJwk(key))) { + throw new InvalidAccessError('Requested operation is only valid for public keys.'); + } + + // The curve specified must be supported by the algorithm implementation processing the operation. + checkValidProperty({ property: key.crv, allowedProperties: this.curves }); + + // The signature must be a Uint8Array. + if (universalTypeOf(signature) !== 'Uint8Array') { + throw new TypeError('The signature must be of type Uint8Array.'); + } + + // The data must be a Uint8Array. + if (universalTypeOf(data) !== 'Uint8Array') { + throw new TypeError('The data must be of type Uint8Array.'); + } + + // If specified, the key's algorithm must match the algorithm implementation processing the operation. + if (key.alg) { + this.checkKeyAlgorithm({ keyAlgorithmName: key.alg }); + } + + // If specified, the key's `key_ops` must include the 'verify' operation. + if (key.key_ops) { + this.checkKeyOperations({ keyOperations: ['verify'], allowedKeyOperations: key.key_ops }); + } } public override async decrypt(): Promise { - throw new InvalidAccessError(`Requested operation 'decrypt' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'decrypt' is not valid for Elliptic Curve algorithms.`); } public override async encrypt(): Promise { - throw new InvalidAccessError(`Requested operation 'encrypt' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'encrypt' is not valid for Elliptic Curve algorithms.`); } public abstract generateKey(options: { algorithm: Web5Crypto.EcGenerateKeyOptions, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise; + keyOperations?: JwkOperation[] + }): Promise; } \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/ec/ecdh.ts b/packages/crypto/src/algorithms-api/ec/ecdh.ts index 4667c3830..414519fab 100644 --- a/packages/crypto/src/algorithms-api/ec/ecdh.ts +++ b/packages/crypto/src/algorithms-api/ec/ecdh.ts @@ -1,4 +1,5 @@ import type { Web5Crypto } from '../../types/web5-crypto.js'; +import { Jose, type JwkOperation, type PrivateKeyJwk } from '../../jose.js'; import { InvalidAccessError } from '../errors.js'; import { BaseEllipticCurveAlgorithm } from './base.js'; @@ -6,48 +7,62 @@ import { checkRequiredProperty } from '../../utils.js'; export abstract class BaseEcdhAlgorithm extends BaseEllipticCurveAlgorithm { - public readonly name: string = 'ECDH'; + public readonly keyOperations: JwkOperation[] = ['deriveBits', 'deriveKey']; - public keyUsages: Web5Crypto.KeyPairUsage = { - privateKey : ['deriveBits', 'deriveKey'], - publicKey : ['deriveBits', 'deriveKey'], - }; - - public checkAlgorithmOptions(options: { + public checkDeriveBitsOptions(options: { algorithm: Web5Crypto.EcdhDeriveKeyOptions, - baseKey: Web5Crypto.CryptoKey + baseKey: PrivateKeyJwk }): void { const { algorithm, baseKey } = options; // Algorithm specified in the operation must match the algorithm implementation processing the operation. this.checkAlgorithmName({ algorithmName: algorithm.name }); + // The algorithm object must contain a publicKey property. checkRequiredProperty({ property: 'publicKey', inObject: algorithm }); - // The publicKey object must be a CryptoKey. - this.checkCryptoKey({ key: algorithm.publicKey }); - // The CryptoKey object must be a public key. - this.checkKeyType({ keyType: algorithm.publicKey.type, allowedKeyType: 'public' }); - // The publicKey algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: algorithm.publicKey.algorithm.name }); + // The publicKey object must be a JSON Web key (JWK). + this.checkJwk({ key: algorithm.publicKey }); + // The publicKey object must be of key type EC or OKP. + this.checkKeyType({ keyType: algorithm.publicKey.kty, allowedKeyTypes: ['EC', 'OKP'] }); + // The publicKey object must be an Elliptic Curve (EC) or Octet Key Pair (OKP) public key in JWK format. + if (!(Jose.isEcPublicKeyJwk(algorithm.publicKey) || Jose.isOkpPublicKeyJwk(algorithm.publicKey))) { + throw new InvalidAccessError(`Requested operation is only valid for public keys.`); + } + // If specified, the public key's `key_ops` must include the 'deriveBits' operation. + if (algorithm.publicKey.key_ops) { + this.checkKeyOperations({ keyOperations: ['deriveBits'], allowedKeyOperations: algorithm.publicKey.key_ops }); + } + // The options object must contain a baseKey property. checkRequiredProperty({ property: 'baseKey', inObject: options }); - // The baseKey object must be a CryptoKey. - this.checkCryptoKey({ key: baseKey }); - // The baseKey algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: baseKey.algorithm.name }); - // The CryptoKey object must be a private key. - this.checkKeyType({ keyType: baseKey.type, allowedKeyType: 'private' }); - // The public and base key named curves must match. - if (('namedCurve' in algorithm.publicKey.algorithm) && ('namedCurve' in baseKey.algorithm) - && (algorithm.publicKey.algorithm.namedCurve !== baseKey.algorithm.namedCurve)) { - throw new InvalidAccessError('The named curve of the publicKey and baseKey must match.'); + // The baseKey object must be a JSON Web Key (JWK). + this.checkJwk({ key: baseKey }); + // The baseKey object must be of key type EC or OKP. + this.checkKeyType({ keyType: baseKey.kty, allowedKeyTypes: ['EC', 'OKP'] }); + // The baseKey object must be an Elliptic Curve (EC) or Octet Key Pair (OKP) private key in JWK format. + if (!(Jose.isEcPrivateKeyJwk(baseKey) || Jose.isOkpPrivateKeyJwk(baseKey))) { + throw new InvalidAccessError(`Requested operation is only valid for private keys.`); + } + // If specified, the base key's `key_ops` must include the 'deriveBits' operation. + if (baseKey.key_ops) { + this.checkKeyOperations({ keyOperations: ['deriveBits'], allowedKeyOperations: baseKey.key_ops }); + } + + // The public and base key types must match. + if ((algorithm.publicKey.kty !== baseKey.kty)) { + throw new InvalidAccessError('The key type of the publicKey and baseKey must match.'); + } + // The public and base key curves must match. + if (('crv' in algorithm.publicKey) && ('crv' in baseKey) + && (algorithm.publicKey.crv !== baseKey.crv)) { + throw new InvalidAccessError('The curve of the publicKey and baseKey must match.'); } } public override async sign(): Promise { - throw new InvalidAccessError(`Requested operation 'sign' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'sign' is not valid for ECDH algorithm.`); } public override async verify(): Promise { - throw new InvalidAccessError(`Requested operation 'verify' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'verify' is not valid for ECDH algorithm.`); } } \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/ec/ecdsa.ts b/packages/crypto/src/algorithms-api/ec/ecdsa.ts index cfec1b22c..74ce4a5ab 100644 --- a/packages/crypto/src/algorithms-api/ec/ecdsa.ts +++ b/packages/crypto/src/algorithms-api/ec/ecdsa.ts @@ -1,37 +1,52 @@ import type { Web5Crypto } from '../../types/web5-crypto.js'; +import type { JwkOperation, PrivateKeyJwk, PublicKeyJwk } from '../../jose.js'; +import { Jose } from '../../jose.js'; import { InvalidAccessError } from '../errors.js'; import { BaseEllipticCurveAlgorithm } from './base.js'; -import { checkValidProperty, checkRequiredProperty } from '../../utils.js'; export abstract class BaseEcdsaAlgorithm extends BaseEllipticCurveAlgorithm { - public readonly name: string = 'ECDSA'; + public readonly keyOperations: JwkOperation[] = ['sign', 'verify']; - public readonly abstract hashAlgorithms: string[]; + public checkSignOptions(options: { + algorithm: Web5Crypto.EcdsaOptions, + key: PrivateKeyJwk, + data: Uint8Array + }): void { + const { key } = options; + + // Input parameter validation that is specified to ECDSA. + if (!Jose.isEcPrivateKeyJwk(key)) { + throw new InvalidAccessError('Requested operation is only valid for EC private keys.'); + } - public readonly keyUsages: Web5Crypto.KeyPairUsage = { - privateKey : ['sign'], - publicKey : ['verify'], - }; + // Input parameter validation that is common to all Elliptic Curve (EC) signature algorithms. + super.checkSignOptions(options); + } - public checkAlgorithmOptions(options: { - algorithm: Web5Crypto.EcdsaOptions + public checkVerifyOptions(options: { + algorithm: Web5Crypto.EcdsaOptions; + key: PublicKeyJwk; + signature: Uint8Array; + data: Uint8Array; }): void { - const { algorithm } = options; - // Algorithm specified in the operation must match the algorithm implementation processing the operation. - this.checkAlgorithmName({ algorithmName: algorithm.name }); - // The algorithm object must contain a hash property. - checkRequiredProperty({ property: 'hash', inObject: algorithm }); - // The hash algorithm specified must be supported by the algorithm implementation processing the operation. - checkValidProperty({ property: algorithm.hash, allowedProperties: this.hashAlgorithms }); + const { key } = options; + + // Input parameter validation that is specified to ECDSA. + if (!Jose.isEcPublicKeyJwk(key)) { + throw new InvalidAccessError('Requested operation is only valid for EC public keys.'); + } + + // Input parameter validation that is common to all Elliptic Curve (EC) signature algorithms. + super.checkVerifyOptions(options); } public override async deriveBits(): Promise { - throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for ECDSA algorithm.`); } - public abstract sign(options: { algorithm: Web5Crypto.EcdsaOptions; key: Web5Crypto.CryptoKey; data: Uint8Array; }): Promise; + public abstract sign(options: { algorithm: Web5Crypto.EcdsaOptions; key: PrivateKeyJwk; data: Uint8Array; }): Promise; - public abstract verify(options: { algorithm: Web5Crypto.EcdsaOptions; key: Web5Crypto.CryptoKey; signature: Uint8Array; data: Uint8Array; }): Promise; + public abstract verify(options: { algorithm: Web5Crypto.EcdsaOptions; key: PublicKeyJwk; signature: Uint8Array; data: Uint8Array; }): Promise; } \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/ec/eddsa.ts b/packages/crypto/src/algorithms-api/ec/eddsa.ts index 75f302221..3592c0db2 100644 --- a/packages/crypto/src/algorithms-api/ec/eddsa.ts +++ b/packages/crypto/src/algorithms-api/ec/eddsa.ts @@ -1,30 +1,52 @@ import type { Web5Crypto } from '../../types/web5-crypto.js'; +import type { JwkOperation, PrivateKeyJwk, PublicKeyJwk } from '../../jose.js'; +import { Jose } from '../../jose.js'; import { InvalidAccessError } from '../errors.js'; import { BaseEllipticCurveAlgorithm } from './base.js'; export abstract class BaseEdDsaAlgorithm extends BaseEllipticCurveAlgorithm { - public readonly name: string = 'EdDSA'; + public readonly keyOperations: JwkOperation[] = ['sign', 'verify']; - public readonly keyUsages: Web5Crypto.KeyPairUsage = { - privateKey : ['sign'], - publicKey : ['verify'], - }; + public checkSignOptions(options: { + algorithm: Web5Crypto.EcdsaOptions, + key: PrivateKeyJwk, + data: Uint8Array + }): void { + const { key } = options; + + // Input parameter validation that is specified to EdDSA. + if (!Jose.isOkpPrivateKeyJwk(key)) { + throw new InvalidAccessError('Requested operation is only valid for OKP private keys.'); + } + + // Input parameter validation that is common to all Elliptic Curve (EC) signature algorithms. + super.checkSignOptions(options); + } - public checkAlgorithmOptions(options: { - algorithm: Web5Crypto.EdDsaOptions + public checkVerifyOptions(options: { + algorithm: Web5Crypto.EcdsaOptions; + key: PublicKeyJwk; + signature: Uint8Array; + data: Uint8Array; }): void { - const { algorithm } = options; - // Algorithm specified in the operation must match the algorithm implementation processing the operation. - this.checkAlgorithmName({ algorithmName: algorithm.name }); + const { key } = options; + + // Input parameter validation that is specified to EdDSA. + if (!Jose.isOkpPublicKeyJwk(key)) { + throw new InvalidAccessError('Requested operation is only valid for OKP public keys.'); + } + + // Input parameter validation that is common to all Elliptic Curve (EC) signature algorithms. + super.checkVerifyOptions(options); } public override async deriveBits(): Promise { - throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for EdDSA algorithm.`); } - public abstract sign(options: { algorithm: Web5Crypto.EdDsaOptions; key: Web5Crypto.CryptoKey; data: Uint8Array; }): Promise; + public abstract sign(options: { algorithm: Web5Crypto.EdDsaOptions; key: PrivateKeyJwk; data: Uint8Array; }): Promise; - public abstract verify(options: { algorithm: Web5Crypto.EdDsaOptions; key: Web5Crypto.CryptoKey; signature: Uint8Array; data: Uint8Array; }): Promise; + public abstract verify(options: { algorithm: Web5Crypto.EdDsaOptions; key: PublicKeyJwk; signature: Uint8Array; data: Uint8Array; }): Promise; } \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/index.ts b/packages/crypto/src/algorithms-api/index.ts index fed29904f..c758b1739 100644 --- a/packages/crypto/src/algorithms-api/index.ts +++ b/packages/crypto/src/algorithms-api/index.ts @@ -1,6 +1,5 @@ export * from './errors.js'; export * from './ec/index.js'; export * from './aes/index.js'; -export * from './crypto-key.js'; export * from './pbkdf/index.js'; export * from './crypto-algorithm.js'; \ No newline at end of file diff --git a/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts b/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts index de5ea703b..54e1f6ba4 100644 --- a/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts +++ b/packages/crypto/src/algorithms-api/pbkdf/pbkdf2.ts @@ -1,21 +1,21 @@ +import { universalTypeOf } from '@web5/common'; + +import type { JwkOperation, PrivateKeyJwk } from '../../jose.js'; import type { Web5Crypto } from '../../types/web5-crypto.js'; -import { InvalidAccessError, OperationError } from '../errors.js'; import { CryptoAlgorithm } from '../crypto-algorithm.js'; +import { InvalidAccessError, OperationError } from '../errors.js'; import { checkRequiredProperty, checkValidProperty } from '../../utils.js'; -import { universalTypeOf } from '@web5/common'; export abstract class BasePbkdf2Algorithm extends CryptoAlgorithm { - public readonly name: string = 'PBKDF2'; - - public readonly abstract hashAlgorithms: string[]; + public readonly abstract hashAlgorithms: ReadonlyArray; - public readonly keyUsages: Web5Crypto.KeyUsage[] = ['deriveBits', 'deriveKey']; + public readonly keyOperations: JwkOperation[] = ['deriveBits', 'deriveKey']; public checkAlgorithmOptions(options: { algorithm: Web5Crypto.Pbkdf2Options, - baseKey: Web5Crypto.CryptoKey + baseKey: PrivateKeyJwk }): void { const { algorithm, baseKey } = options; // Algorithm specified in the operation must match the algorithm implementation processing the operation. @@ -42,50 +42,29 @@ export abstract class BasePbkdf2Algorithm extends CryptoAlgorithm { } // The options object must contain a baseKey property. checkRequiredProperty({ property: 'baseKey', inObject: options }); - // The baseKey object must be a CryptoKey. - this.checkCryptoKey({ key: baseKey }); - // The baseKey algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: baseKey.algorithm.name }); - } - - public checkImportKey(options: { - algorithm: Web5Crypto.Algorithm, - format: Web5Crypto.KeyFormat, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): void { - const { algorithm, format, extractable, keyUsages } = options; - // Algorithm specified in the operation must match the algorithm implementation processing the operation. - this.checkAlgorithmName({ algorithmName: algorithm.name }); - // The format specified must be 'raw'. - if (format !== 'raw') { - throw new SyntaxError(`Format '${format}' not supported. Only 'raw' is supported.`); - } - // The extractable value specified must be false. - if (extractable !== false) { - throw new SyntaxError(`Extractable '${extractable}' not supported. Only 'false' is supported.`); - } - // The key usages specified must be permitted by the algorithm implementation processing the operation. - this.checkKeyUsages({ keyUsages, allowedKeyUsages: this.keyUsages }); + // The baseKey object must be a JSON Web Key (JWK). + this.checkJwk({ key: baseKey }); + // The baseKey must be of type 'oct' (octet sequence). + this.checkKeyType({ keyType: baseKey.kty, allowedKeyTypes: ['oct'] }); } public override async decrypt(): Promise { - throw new InvalidAccessError(`Requested operation 'decrypt' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'decrypt' is not valid for '${this.names.join(', ')}' keys.`); } public override async encrypt(): Promise { - throw new InvalidAccessError(`Requested operation 'encrypt' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'encrypt' is not valid for '${this.names.join(', ')}' keys.`); } - public override async generateKey(): Promise { - throw new InvalidAccessError(`Requested operation 'generateKey' is not valid for ${this.name} keys.`); + public override async generateKey(): Promise { + throw new InvalidAccessError(`Requested operation 'generateKey' is not valid for '${this.names.join(', ')}' keys.`); } public override async sign(): Promise { - throw new InvalidAccessError(`Requested operation 'sign' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'sign' is not valid for '${this.names.join(', ')}' keys.`); } public override async verify(): Promise { - throw new InvalidAccessError(`Requested operation 'verify' is not valid for ${this.name} keys.`); + throw new InvalidAccessError(`Requested operation 'verify' is not valid for '${this.names.join(', ')}' keys.`); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/aes-ctr.ts b/packages/crypto/src/crypto-algorithms/aes-ctr.ts index 8567f830f..366201b88 100644 --- a/packages/crypto/src/crypto-algorithms/aes-ctr.ts +++ b/packages/crypto/src/crypto-algorithms/aes-ctr.ts @@ -1,26 +1,26 @@ -import { universalTypeOf } from '@web5/common'; - import type { Web5Crypto } from '../types/web5-crypto.js'; +import type{ JwkOperation, PrivateKeyJwk } from '../jose.js'; import { AesCtr } from '../crypto-primitives/index.js'; -import { BaseAesCtrAlgorithm, CryptoKey } from '../algorithms-api/index.js'; +import { BaseAesCtrAlgorithm } from '../algorithms-api/index.js'; export class AesCtrAlgorithm extends BaseAesCtrAlgorithm { + public readonly names = ['A128CTR', 'A192CTR', 'A256CTR'] as const; + public async decrypt(options: { algorithm: Web5Crypto.AesCtrOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise { const { algorithm, key, data } = options; - this.checkAlgorithmOptions({ algorithm, key }); - // The secret key must be allowed to be used for 'decrypt' operations. - this.checkKeyUsages({ keyUsages: ['decrypt'], allowedKeyUsages: key.usages }); + // Validate the input parameters. + this.checkDecryptOptions(options); const plaintext = AesCtr.decrypt({ counter : algorithm.counter, data : data, - key : key.material, + key : key, length : algorithm.length }); @@ -29,19 +29,18 @@ export class AesCtrAlgorithm extends BaseAesCtrAlgorithm { public async encrypt(options: { algorithm: Web5Crypto.AesCtrOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise { const { algorithm, key, data } = options; - this.checkAlgorithmOptions({ algorithm, key }); - // The secret key must be allowed to be used for 'encrypt' operations. - this.checkKeyUsages({ keyUsages: ['encrypt'], allowedKeyUsages: key.usages }); + // Validate the input parameters. + this.checkEncryptOptions(options); const ciphertext = AesCtr.encrypt({ counter : algorithm.counter, data : data, - key : key.material, + key : key, length : algorithm.length }); @@ -50,21 +49,28 @@ export class AesCtrAlgorithm extends BaseAesCtrAlgorithm { public async generateKey(options: { algorithm: Web5Crypto.AesGenerateKeyOptions, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise { - const { algorithm, extractable, keyUsages } = options; - - this.checkGenerateKey({ algorithm, keyUsages }); - - const secretKey = await AesCtr.generateKey({ length: algorithm.length }); - - if (universalTypeOf(secretKey) !== 'Uint8Array') { - throw new Error('Operation failed to generate key.'); + keyOperations: JwkOperation[] + }): Promise { + const { algorithm, keyOperations } = options; + + // Validate the input parameters. + this.checkGenerateKeyOptions({ algorithm, keyOperations }); + + // Map algorithm name to key length. + const algorithmNameToLength: Record = { + A128CTR : 128, + A192CTR : 192, + A256CTR : 256 + }; + + const secretKey = await AesCtr.generateKey({ length: algorithmNameToLength[algorithm.name] }); + + if (secretKey) { + secretKey.alg = algorithm.name; + if (keyOperations) secretKey.key_ops = keyOperations; + return secretKey; } - const secretCryptoKey = new CryptoKey(algorithm, extractable, secretKey, 'secret', this.keyUsages); - - return secretCryptoKey; + throw new Error('Operation failed: generateKey'); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/ecdh.ts b/packages/crypto/src/crypto-algorithms/ecdh.ts index a87fa67be..e9d6e00c8 100644 --- a/packages/crypto/src/crypto-algorithms/ecdh.ts +++ b/packages/crypto/src/crypto-algorithms/ecdh.ts @@ -1,70 +1,70 @@ import type { Web5Crypto } from '../types/web5-crypto.js'; -import type { BytesKeyPair } from '../types/crypto-key.js'; +import type { + JwkOperation, + PrivateKeyJwk, + JwkParamsEcPrivate, + JwkParamsOkpPrivate, +} from '../jose.js'; -import { isBytesKeyPair } from '../utils.js'; import { Secp256k1, X25519 } from '../crypto-primitives/index.js'; -import { CryptoKey, BaseEcdhAlgorithm, OperationError } from '../algorithms-api/index.js'; +import { BaseEcdhAlgorithm, OperationError } from '../algorithms-api/index.js'; export class EcdhAlgorithm extends BaseEcdhAlgorithm { - public readonly namedCurves = ['secp256k1', 'X25519']; + public readonly names = ['ECDH'] as const; + public readonly curves = ['secp256k1', 'X25519'] as const; public async deriveBits(options: { algorithm: Web5Crypto.EcdhDeriveKeyOptions, - baseKey: Web5Crypto.CryptoKey, - length: number | null + baseKey: PrivateKeyJwk, + length?: number }): Promise { const { algorithm, baseKey, length } = options; - this.checkAlgorithmOptions({ algorithm, baseKey }); - // The base key must be allowed to be used for deriveBits operations. - this.checkKeyUsages({ keyUsages: ['deriveBits'], allowedKeyUsages: baseKey.usages }); - // The public key must be allowed to be used for deriveBits operations. - this.checkKeyUsages({ keyUsages: ['deriveBits'], allowedKeyUsages: algorithm.publicKey.usages }); + // Validate the input parameters. + this.checkDeriveBitsOptions({ algorithm, baseKey }); let sharedSecret: Uint8Array; + const curve = (baseKey as JwkParamsEcPrivate | JwkParamsOkpPrivate).crv; // checkDeriveBitsOptions verifies that the base key is of type EC or OKP. - const ownKeyAlgorithm = baseKey.algorithm as Web5Crypto.EcGenerateKeyOptions; // Type guard. - - switch (ownKeyAlgorithm.namedCurve) { + switch (curve) { case 'secp256k1': { - const ownPrivateKey = baseKey.material; - const otherPartyPublicKey = algorithm.publicKey.material; sharedSecret = await Secp256k1.sharedSecret({ - privateKey : ownPrivateKey, - publicKey : otherPartyPublicKey + privateKeyA : baseKey, + publicKeyB : algorithm.publicKey }); break; } case 'X25519': { - const ownPrivateKey = baseKey.material; - const otherPartyPublicKey = algorithm.publicKey.material; sharedSecret = await X25519.sharedSecret({ - privateKey : ownPrivateKey, - publicKey : otherPartyPublicKey + privateKeyA : baseKey, + publicKeyB : algorithm.publicKey }); break; } - default: - throw new TypeError(`Out of range: '${ownKeyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`); + default: { + throw new TypeError(`Out of range: '${curve}'. Must be one of '${this.curves.join(', ')}'`); + } } - // Length is null, return the full derived secret. - if (length === null) + // If 'length' is not specified, return the full derived secret. + if (length === undefined) return sharedSecret; // If the length is not a multiple of 8, throw. - if (length && length % 8 !== 0) + if (length && length % 8 !== 0) { throw new OperationError(`To be compatible with all browsers, 'length' must be a multiple of 8.`); + } // Convert length from bits to bytes. const lengthInBytes = length / 8; // If length (converted to bytes) is larger than the derived secret, throw. - if (sharedSecret.byteLength < lengthInBytes) + if (sharedSecret.byteLength < lengthInBytes) { throw new OperationError(`Requested 'length' exceeds the byte length of the derived secret.`); + } // Otherwise, either return the secret or a truncated slice. return lengthInBytes === sharedSecret.byteLength ? @@ -73,43 +73,35 @@ export class EcdhAlgorithm extends BaseEcdhAlgorithm { } public async generateKey(options: { - algorithm: Web5Crypto.EcGenerateKeyOptions | Web5Crypto.EcdsaGenerateKeyOptions, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise { - const { algorithm, extractable, keyUsages } = options; + algorithm: Web5Crypto.EcGenerateKeyOptions, + keyOperations?: JwkOperation[] + }): Promise { + const { algorithm, keyOperations } = options; - this.checkGenerateKey({ algorithm, keyUsages }); + // Validate the input parameters. + this.checkGenerateKeyOptions({ algorithm, keyOperations }); - let keyPair: BytesKeyPair | undefined; - let cryptoKeyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk | undefined; - switch (algorithm.namedCurve) { + switch (algorithm.curve) { case 'secp256k1': { - (algorithm as Web5Crypto.EcdsaGenerateKeyOptions).compressedPublicKey ??= true; - keyPair = await Secp256k1.generateKeyPair({ - compressedPublicKey: (algorithm as Web5Crypto.EcdsaGenerateKeyOptions).compressedPublicKey - }); + privateKey = await Secp256k1.generateKey(); break; } case 'X25519': { - keyPair = await X25519.generateKeyPair(); + privateKey = await X25519.generateKey(); break; } - // Default case not needed because checkGenerateKey() already validates the specified namedCurve is supported. + // Default case not needed because checkGenerateKeyOptions() already validates the specified curve is supported. } - if (!isBytesKeyPair(keyPair)) { - throw new Error('Operation failed to generate key pair.'); + if (privateKey) { + if (keyOperations) privateKey.key_ops = keyOperations; + return privateKey; } - cryptoKeyPair = { - privateKey : new CryptoKey(algorithm, extractable, keyPair.privateKey, 'private', this.keyUsages.privateKey), - publicKey : new CryptoKey(algorithm, true, keyPair.publicKey, 'public', this.keyUsages.publicKey) - }; - - return cryptoKeyPair; + throw new Error('Operation failed: generateKey'); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/ecdsa.ts b/packages/crypto/src/crypto-algorithms/ecdsa.ts index 828a42a6a..bda521dbe 100644 --- a/packages/crypto/src/crypto-algorithms/ecdsa.ts +++ b/packages/crypto/src/crypto-algorithms/ecdsa.ts @@ -1,111 +1,86 @@ import type { Web5Crypto } from '../types/web5-crypto.js'; -import type { BytesKeyPair } from '../types/crypto-key.js'; +import type { JwkOperation, JwkParamsEcPrivate, JwkParamsEcPublic, PrivateKeyJwk, PublicKeyJwk } from '../jose.js'; -import { isBytesKeyPair } from '../utils.js'; import { Secp256k1 } from '../crypto-primitives/index.js'; -import { CryptoKey, BaseEcdsaAlgorithm } from '../algorithms-api/index.js'; +import { BaseEcdsaAlgorithm } from '../algorithms-api/index.js'; + export class EcdsaAlgorithm extends BaseEcdsaAlgorithm { - public readonly hashAlgorithms = ['SHA-256']; - public readonly namedCurves = ['secp256k1']; + public readonly names = ['ES256K'] as const; + public readonly curves = ['secp256k1'] as const; public async generateKey(options: { algorithm: Web5Crypto.EcdsaGenerateKeyOptions, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise { - const { algorithm, extractable, keyUsages } = options; + keyOperations?: JwkOperation[] + }): Promise { + const { algorithm, keyOperations } = options; - this.checkGenerateKey({ algorithm, keyUsages }); + // Validate the input parameters. + this.checkGenerateKeyOptions({ algorithm, keyOperations }); - let keyPair: BytesKeyPair | undefined; - let cryptoKeyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk | undefined; - switch (algorithm.namedCurve) { + switch (algorithm.curve) { case 'secp256k1': { - algorithm.compressedPublicKey ??= true; - keyPair = await Secp256k1.generateKeyPair({ compressedPublicKey: algorithm.compressedPublicKey }); + privateKey = await Secp256k1.generateKey(); + privateKey.alg = 'ES256K'; break; } - // Default case not needed because checkGenerateKey() already validates the specified namedCurve is supported. + // Default case unnecessary because checkGenerateKeyOptions() validates the input parameters. } - if (!isBytesKeyPair(keyPair)) { - throw new Error('Operation failed to generate key pair.'); + if (privateKey) { + if (keyOperations) privateKey.key_ops = keyOperations; + return privateKey; } - cryptoKeyPair = { - privateKey : new CryptoKey(algorithm, extractable, keyPair.privateKey, 'private', this.keyUsages.privateKey), - publicKey : new CryptoKey(algorithm, true, keyPair.publicKey, 'public', this.keyUsages.publicKey) - }; - - return cryptoKeyPair; + throw new Error('Operation failed: generateKey'); } public async sign(options: { algorithm: Web5Crypto.EcdsaOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise { - const { algorithm, key, data } = options; + const { key, data } = options; - this.checkAlgorithmOptions({ algorithm }); - // The key's algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name }); - // The key must be a private key. - this.checkKeyType({ keyType: key.type, allowedKeyType: 'private' }); - // The key must be allowed to be used for sign operations. - this.checkKeyUsages({ keyUsages: ['sign'], allowedKeyUsages: key.usages }); + // Validate the input parameters. + this.checkSignOptions(options); - let signature: Uint8Array; + const curve = (key as JwkParamsEcPrivate).crv; // checkSignOptions verifies that the key is an EC private key. - const keyAlgorithm = key.algorithm as Web5Crypto.EcdsaGenerateKeyOptions; // Type guard. - - switch (keyAlgorithm.namedCurve) { + switch (curve) { case 'secp256k1': { - signature = await Secp256k1.sign({ hash: algorithm.hash, key: key.material, data }); - break; + return await Secp256k1.sign({ key, data }); } - - default: - throw new TypeError(`Out of range: '${keyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`); + // Default case unnecessary because checkSignOptions() validates the input parameters. } - return signature; + throw new Error('Operation failed: sign'); } public async verify(options: { algorithm: Web5Crypto.EcdsaOptions; - key: Web5Crypto.CryptoKey; + key: PublicKeyJwk; signature: Uint8Array; data: Uint8Array; }): Promise { - const { algorithm, key, signature, data } = options; - - this.checkAlgorithmOptions({ algorithm }); - // The key's algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name }); - // The key must be a public key. - this.checkKeyType({ keyType: key.type, allowedKeyType: 'public' }); - // The key must be allowed to be used for verify operations. - this.checkKeyUsages({ keyUsages: ['verify'], allowedKeyUsages: key.usages }); + const { key, signature, data } = options; - let isValid: boolean; + // Validate the input parameters. + this.checkVerifyOptions(options); - const keyAlgorithm = key.algorithm as Web5Crypto.EcdsaGenerateKeyOptions; // Type guard. + const curve = (key as JwkParamsEcPublic).crv; // checkVerifyOptions verifies that the key is an EC public key. - switch (keyAlgorithm.namedCurve) { + switch (curve) { case 'secp256k1': { - isValid = await Secp256k1.verify({ hash: algorithm.hash, key: key.material, signature, data }); - break; + return await Secp256k1.verify({ key, signature, data }); } - - default: - throw new TypeError(`Out of range: '${keyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`); + // Default case unnecessary because checkVerifyOptions() validates the input parameters. } - return isValid; + throw new Error('Operation failed: verify'); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/eddsa.ts b/packages/crypto/src/crypto-algorithms/eddsa.ts index 1932443ba..ffcf9d45d 100644 --- a/packages/crypto/src/crypto-algorithms/eddsa.ts +++ b/packages/crypto/src/crypto-algorithms/eddsa.ts @@ -1,110 +1,86 @@ import type { Web5Crypto } from '../types/web5-crypto.js'; -import type { BytesKeyPair } from '../types/crypto-key.js'; +import type { JwkOperation, JwkParamsOkpPrivate, JwkParamsOkpPublic, PrivateKeyJwk, PublicKeyJwk } from '../jose.js'; -import { isBytesKeyPair } from '../utils.js'; import { Ed25519 } from '../crypto-primitives/index.js'; -import { CryptoKey, BaseEdDsaAlgorithm } from '../algorithms-api/index.js'; +import { BaseEdDsaAlgorithm } from '../algorithms-api/index.js'; export class EdDsaAlgorithm extends BaseEdDsaAlgorithm { - public readonly namedCurves = ['Ed25519', 'Ed448']; + public readonly names = ['EdDSA'] as const; + public readonly curves = ['Ed25519'] as const; public async generateKey(options: { algorithm: Web5Crypto.EdDsaGenerateKeyOptions, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise { - const { algorithm, extractable, keyUsages } = options; + keyOperations?: JwkOperation[] + }): Promise { + const { algorithm, keyOperations } = options; - this.checkGenerateKey({ algorithm, keyUsages }); + // Validate the input parameters. + this.checkGenerateKeyOptions({ algorithm, keyOperations }); - let keyPair: BytesKeyPair | undefined; - let cryptoKeyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk | undefined; - switch (algorithm.namedCurve) { + switch (algorithm.curve) { case 'Ed25519': { - keyPair = await Ed25519.generateKeyPair(); + privateKey = await Ed25519.generateKey(); + privateKey.alg = 'EdDSA'; break; } - // Default case not needed because checkGenerateKey() already validates the specified namedCurve is supported. + // Default case unnecessary because checkGenerateKeyOptions() validates the input parameters. } - if (!isBytesKeyPair(keyPair)) { - throw new Error('Operation failed to generate key pair.'); + if (privateKey) { + if (keyOperations) privateKey.key_ops = keyOperations; + return privateKey; } - cryptoKeyPair = { - privateKey : new CryptoKey(algorithm, extractable, keyPair.privateKey, 'private', this.keyUsages.privateKey), - publicKey : new CryptoKey(algorithm, true, keyPair.publicKey, 'public', this.keyUsages.publicKey) - }; - - return cryptoKeyPair; + throw new Error('Operation failed: generateKey'); } public async sign(options: { algorithm: Web5Crypto.EdDsaOptions, - key: Web5Crypto.CryptoKey, + key: PrivateKeyJwk, data: Uint8Array }): Promise { - const { algorithm, key, data } = options; - - this.checkAlgorithmOptions({ algorithm }); - // The key's algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name }); - // The key must be a private key. - this.checkKeyType({ keyType: key.type, allowedKeyType: 'private' }); - // The key must be allowed to be used for sign operations. - this.checkKeyUsages({ keyUsages: ['sign'], allowedKeyUsages: key.usages }); + const { key, data } = options; - let signature: Uint8Array; + // Validate the input parameters. + this.checkSignOptions(options); - const keyAlgorithm = key.algorithm as Web5Crypto.EdDsaGenerateKeyOptions; // Type guard. + const curve = (key as JwkParamsOkpPrivate).crv; // checkSignOptions verifies that the key is an OKP private key. - switch (keyAlgorithm.namedCurve) { + switch (curve) { case 'Ed25519': { - signature = await Ed25519.sign({ key: key.material, data }); - break; + return await Ed25519.sign({ key, data }); } - - default: - throw new TypeError(`Out of range: '${keyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`); + // Default case unnecessary because checkSignOptions() validates the input parameters. } - return signature; + throw new Error('Operation failed: sign'); } public async verify(options: { algorithm: Web5Crypto.EdDsaOptions; - key: Web5Crypto.CryptoKey; + key: PublicKeyJwk; signature: Uint8Array; data: Uint8Array; }): Promise { - const { algorithm, key, signature, data } = options; + const { key, signature, data } = options; - this.checkAlgorithmOptions({ algorithm }); - // The key's algorithm must match the algorithm implementation processing the operation. - this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name }); - // The key must be a public key. - this.checkKeyType({ keyType: key.type, allowedKeyType: 'public' }); - // The key must be allowed to be used for verify operations. - this.checkKeyUsages({ keyUsages: ['verify'], allowedKeyUsages: key.usages }); + // Validate the input parameters. + this.checkVerifyOptions(options); - let isValid: boolean; + const curve = (key as JwkParamsOkpPublic).crv; // checkVerifyOptions verifies that the key is an OKP public key. - const keyAlgorithm = key.algorithm as Web5Crypto.EdDsaGenerateKeyOptions; // Type guard. - - switch (keyAlgorithm.namedCurve) { + switch (curve) { case 'Ed25519': { - isValid = await Ed25519.verify({ key: key.material, signature, data }); - break; + return await Ed25519.verify({ key, signature, data }); } - - default: - throw new TypeError(`Out of range: '${keyAlgorithm.namedCurve}'. Must be one of '${this.namedCurves.join(', ')}'`); + // Default case unnecessary because checkVerifyOptions() validates the input parameters. } - return isValid; + throw new Error('Operation failed: verify'); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-algorithms/pbkdf2.ts b/packages/crypto/src/crypto-algorithms/pbkdf2.ts index 68f4aa12f..782c28c5e 100644 --- a/packages/crypto/src/crypto-algorithms/pbkdf2.ts +++ b/packages/crypto/src/crypto-algorithms/pbkdf2.ts @@ -1,54 +1,51 @@ +import { Convert } from '@web5/common'; + +import type { JwkParamsOctPrivate, PrivateKeyJwk } from '../jose.js'; import type { Web5Crypto } from '../types/web5-crypto.js'; -import { BasePbkdf2Algorithm, CryptoKey, OperationError } from '../algorithms-api/index.js'; import { Pbkdf2 } from '../crypto-primitives/pbkdf2.js'; +import { BasePbkdf2Algorithm, OperationError } from '../algorithms-api/index.js'; export class Pbkdf2Algorithm extends BasePbkdf2Algorithm { - public readonly hashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512']; + public readonly names = ['PBKDF2'] as const; + public readonly hashAlgorithms = ['SHA-256', 'SHA-384', 'SHA-512'] as const; public async deriveBits(options: { algorithm: Web5Crypto.Pbkdf2Options, - baseKey: Web5Crypto.CryptoKey, + baseKey: PrivateKeyJwk, length: number }): Promise { const { algorithm, baseKey, length } = options; + // Check the `algorithm` and `baseKey` values for PBKDF2 requirements. this.checkAlgorithmOptions({ algorithm, baseKey }); - // The base key must be allowed to be used for deriveBits operations. - this.checkKeyUsages({ keyUsages: ['deriveBits'], allowedKeyUsages: baseKey.usages }); + + // If specified, the base key's `key_ops` must include the 'deriveBits' operation. + if (baseKey.key_ops) { + this.checkKeyOperations({ keyOperations: ['deriveBits'], allowedKeyOperations: baseKey.key_ops }); + } + // If the length is 0, throw. if (typeof length !== 'undefined' && length === 0) { throw new OperationError(`The value of 'length' cannot be zero.`); } + // If the length is not a multiple of 8, throw. if (length && length % 8 !== 0) { throw new OperationError(`To be compatible with all browsers, 'length' must be a multiple of 8.`); } + // Convert the base key to bytes. + const baseKeyBytes = Convert.base64Url((baseKey as JwkParamsOctPrivate).k).toUint8Array(); + const derivedBits = Pbkdf2.deriveKey({ hash : algorithm.hash as 'SHA-256' | 'SHA-384' | 'SHA-512', iterations : algorithm.iterations, length : length, - password : baseKey.material, + password : baseKeyBytes, salt : algorithm.salt }); return derivedBits; } - - public async importKey(options: { - format: Web5Crypto.KeyFormat, - keyData: Uint8Array, - algorithm: Web5Crypto.Algorithm, - extractable: boolean, - keyUsages: Web5Crypto.KeyUsage[] - }): Promise { - const { format, keyData, algorithm, extractable, keyUsages } = options; - - this.checkImportKey({ algorithm, format, extractable, keyUsages }); - - const cryptoKey = new CryptoKey(algorithm, extractable, keyData, 'secret', keyUsages); - - return cryptoKey; - } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/aes-ctr.ts b/packages/crypto/src/crypto-primitives/aes-ctr.ts index f218bdf63..2019c07c6 100644 --- a/packages/crypto/src/crypto-primitives/aes-ctr.ts +++ b/packages/crypto/src/crypto-primitives/aes-ctr.ts @@ -1,50 +1,137 @@ +import { Convert } from '@web5/common'; import { crypto } from '@noble/hashes/crypto'; +import type { PrivateKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; + /** - * The `AesCtr` class provides an interface for AES-CTR - * (Advanced Encryption Standard - Counter) encryption and decryption - * operations. The class uses the Web Crypto API for cryptographic operations. + * The `AesCtr` class provides a comprehensive set of utilities for cryptographic operations + * using the Advanced Encryption Standard (AES) in Counter (CTR) mode. This class includes + * methods for key generation, encryption, decryption, and conversions between raw byte arrays + * and JSON Web Key (JWK) formats. It is designed to support AES-CTR, a symmetric key algorithm + * that is widely used in various cryptographic applications for its efficiency and security. + * + * AES-CTR mode operates as a stream cipher using a block cipher (AES) and is well-suited for + * scenarios where parallel processing is beneficial or where the same key is required to + * encrypt multiple data blocks. The class adheres to standard cryptographic practices, ensuring + * compatibility and security in its implementations. + * + * Key Features: + * - Key Generation: Generate AES symmetric keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Encryption: Encrypt data using AES-CTR with the provided symmetric key. + * - Decryption: Decrypt data encrypted with AES-CTR using the corresponding symmetric key. * - * All methods of this class are asynchronous and return Promises. They all - * use the Uint8Array type for keys and data, providing a consistent - * interface for working with binary data. + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments. * - * Example usage: + * Usage Examples: * * ```ts - * const key = await AesCtr.generateKey({ length: 128 }); - * const counter = new Uint8Array(16); // initialize a 16-byte counter - * const message = new TextEncoder().encode('Hello, world!'); - * const ciphertext = await AesCtr.encrypt({ + * // Key Generation + * const length = 256; // Length of the key in bits (e.g., 128, 192, 256) + * const privateKey = await AesCtr.generateKey({ length }); + * + * // Encryption + * const data = new TextEncoder().encode('Hello, world!'); + * const counter = new Uint8Array(16); // 16-byte (128-bit) counter block + * const encryptedData = await AesCtr.encrypt({ + * data, * counter, - * data: message, - * key, - * length: 128 // counter length in bits + * key: privateKey, + * length: 128 // Length of the counter block in bits * }); - * const plaintext = await AesCtr.decrypt({ + * + * // Decryption + * const decryptedData = await AesCtr.decrypt({ + * data: encryptedData, * counter, - * data: ciphertext, - * key, - * length: 128 // counter length in bits + * key: privateKey, + * length: 128 // Length of the counter block in bits * }); - * console.log(new TextDecoder().decode(plaintext)); // 'Hello, world!' + * + * // Key Conversion + * const privateKeyBytes = await AesCtr.privateKeyToBytes({ privateKey }); * ``` */ export class AesCtr { /** - * Decrypts the provided data using AES-CTR. + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method takes a symmetric key represented as a byte array (Uint8Array) and + * converts it into a JWK object for use with AES (Advanced Encryption Standard) + * in Counter (CTR) mode. The conversion process involves encoding the key into + * base64url format and setting the appropriate JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'oct' for Octet Sequence (representing a symmetric key). + * - `k`: The symmetric key, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual symmetric key bytes + * const privateKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKeyBytes - The raw symmetric key as a Uint8Array. + * + * @returns A Promise that resolves to the symmetric key in JWK format. + */ + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + k : Convert.uint8Array(privateKeyBytes).toBase64Url(), + kty : 'oct' + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Decrypts the provided data using AES in Counter (CTR) mode. + * + * This method performs AES-CTR decryption on the given encrypted data using the specified key. + * Similar to the encryption process, it requires an initial counter block and the length + * of the counter block, along with the encrypted data and the decryption key. The method + * returns the decrypted data as a Uint8Array. + * + * Example usage: + * + * ```ts + * const encryptedData = new Uint8Array([...]); // Encrypted data + * const counter = new Uint8Array(16); // 16-byte (128-bit) counter block used during encryption + * const key = { ... }; // A PrivateKeyJwk object representing the same AES key used for encryption + * const decryptedData = await AesCtr.decrypt({ + * data: encryptedData, + * counter, + * key, + * length: 128 // Length of the counter block in bits + * }); + * ``` * * @param options - The options for the decryption operation. * @param options.counter - The initial value of the counter block. - * @param options.data - The data to decrypt. - * @param options.key - The key to use for decryption. + * @param options.data - The encrypted data to decrypt, represented as a Uint8Array. + * @param options.key - The key to use for decryption, represented in JWK format. * @param options.length - The length of the counter block in bits. + * * @returns A Promise that resolves to the decrypted data as a Uint8Array. */ public static async decrypt(options: { counter: Uint8Array, data: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, length: number }): Promise { const { counter, data, key, length } = options; @@ -64,19 +151,39 @@ export class AesCtr { } /** - * Encrypts the provided data using AES-CTR. + * Encrypts the provided data using AES in Counter (CTR) mode. + * + * This method performs AES-CTR encryption on the given data using the specified key. + * It requires the initial counter block and the length of the counter block, alongside + * the data and key. The method is designed to work asynchronously and returns the + * encrypted data as a Uint8Array. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); + * const counter = new Uint8Array(16); // 16-byte (128-bit) counter block + * const key = { ... }; // A PrivateKeyJwk object representing an AES key + * const encryptedData = await AesCtr.encrypt({ + * data, + * counter, + * key, + * length: 128 // Length of the counter block in bits + * }); + * ``` * * @param options - The options for the encryption operation. * @param options.counter - The initial value of the counter block. - * @param options.data - The data to encrypt. - * @param options.key - The key to use for encryption. + * @param options.data - The data to encrypt, represented as a Uint8Array. + * @param options.key - The key to use for encryption, represented in JWK format. * @param options.length - The length of the counter block in bits. + * * @returns A Promise that resolves to the encrypted data as a Uint8Array. */ public static async encrypt(options: { counter: Uint8Array, data: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, length: number }): Promise { const { counter, data, key, length } = options; @@ -96,36 +203,97 @@ export class AesCtr { } /** - * Generates an AES key of a given length. + * Generates a symmetric key for AES in Counter (CTR) mode in JSON Web Key (JWK) format. + * + * This method creates a new symmetric key of a specified length suitable for use with + * AES-CTR encryption. It uses cryptographically secure random number generation to + * ensure the uniqueness and security of the key. The generated key adheres to the JWK + * format, making it compatible with common cryptographic standards and easy to use in + * various cryptographic processes. + * + * The generated key includes the following components: + * - `kty`: Key Type, set to 'oct' for Octet Sequence. + * - `k`: The symmetric key component, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: * - * @param length - The length of the key in bits. - * @returns A Promise that resolves to the generated key as a Uint8Array. + * ```ts + * const length = 256; // Length of the key in bits (e.g., 128, 192, 256) + * const privateKey = await AesCtr.generateKey({ length }); + * ``` + * + * @param options - The options for the key generation. + * @param options.length - The length of the key in bits. Common lengths are 128, 192, and 256 bits. + * + * @returns A Promise that resolves to the generated symmetric key in JWK format. */ public static async generateKey(options: { length: number - }): Promise { + }): Promise { const { length } = options; - // Generate the secret key. + // Generate a random private key. const lengthInBytes = length / 8; - const secretKey = crypto.getRandomValues(new Uint8Array(lengthInBytes)); + const privateKeyBytes = crypto.getRandomValues(new Uint8Array(lengthInBytes)); + + // Convert private key from bytes to JWK format. + const privateKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method takes a symmetric key in JWK format and extracts its raw byte representation. + * It decodes the 'k' parameter of the JWK value, which represents the symmetric key in base64url + * encoding, into a byte array. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // A symmetric key in JWK format + * const privateKeyBytes = await AesCtr.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKey - The symmetric key in JWK format. + * + * @returns A Promise that resolves to the symmetric key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Verify the provided JWK represents a valid oct private key. + if (!Jose.isOctPrivateKeyJwk(privateKey)) { + throw new Error(`AesCtr: The provided key is not a valid oct private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); - return secretKey; + return privateKeyBytes; } /** - * A private method to import a raw key for use with the Web Crypto API. + * A private method to import a key in JWK format for use with the Web Crypto API. * - * @param key - The raw key material. + * @param key - The key in JWK format. * @returns A Promise that resolves to a CryptoKey. */ - private static async importKey(key: Uint8Array): Promise { + private static async importKey(key: PrivateKeyJwk): Promise { return crypto.subtle.importKey( - 'raw', - key.buffer, - { name: 'AES-CTR', length: key.byteLength * 8 }, - true, - ['encrypt', 'decrypt'] + 'jwk', // format + key, // keyData + { name: 'AES-CTR' }, // algorithm + true, // extractable + ['encrypt', 'decrypt'] // usages ); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/aes-gcm.ts b/packages/crypto/src/crypto-primitives/aes-gcm.ts index 852bb15e7..137463c32 100644 --- a/packages/crypto/src/crypto-primitives/aes-gcm.ts +++ b/packages/crypto/src/crypto-primitives/aes-gcm.ts @@ -1,53 +1,142 @@ +import { Convert } from '@web5/common'; import { crypto } from '@noble/hashes/crypto'; +import type { PrivateKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; + /** - * The `AesGcm` class provides an interface for AES-GCM - * (Advanced Encryption Standard - Galois/Counter Mode) encryption and - * decryption operations. The class uses the Web Crypto API for - * cryptographic operations. + * The `AesGcm` class provides a comprehensive set of utilities for cryptographic operations + * using the Advanced Encryption Standard (AES) in Galois/Counter Mode (GCM). This class includes + * methods for key generation, encryption, decryption, and conversions between raw byte arrays + * and JSON Web Key (JWK) formats. It is designed to support AES-GCM, a symmetric key algorithm + * that is widely used for its efficiency, security, and provision of authenticated encryption. * - * All methods of this class are asynchronous and return Promises. They all - * use the Uint8Array type for keys and data, providing a consistent - * interface for working with binary data. + * AES-GCM is particularly favored for scenarios that require both confidentiality and integrity + * of data. It integrates the counter mode of encryption with the Galois mode of authentication, + * offering high performance and parallel processing capabilities. * - * Example usage: + * Key Features: + * - Key Generation: Generate AES symmetric keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Encryption: Encrypt data using AES-GCM with the provided symmetric key. + * - Decryption: Decrypt data encrypted with AES-GCM using the corresponding symmetric key. + * + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments. + * + * Usage Examples: * * ```ts - * const key = await AesGcm.generateKey({ length: 128 }); - * const iv = new Uint8Array(12); // generate a 12-byte initialization vector - * const message = new TextEncoder().encode('Hello, world!'); - * const ciphertext = await AesGcm.encrypt({ - * data: message, + * // Key Generation + * const length = 256; // Length of the key in bits (e.g., 128, 192, 256) + * const privateKey = await AesGcm.generateKey({ length }); + * + * // Encryption + * const data = new TextEncoder().encode('Hello, world!'); + * const iv = new Uint8Array(12); // 12-byte initialization vector + * const encryptedData = await AesGcm.encrypt({ + * data, * iv, - * key, - * tagLength: 128 + * key: privateKey, + * tagLength: 128 // Length of the authentication tag in bits * }); - * const plaintext = await AesGcm.decrypt({ - * data: ciphertext, + * + * // Decryption + * const decryptedData = await AesGcm.decrypt({ + * data: encryptedData, * iv, - * key, - * tagLength: 128 + * key: privateKey, + * tagLength: 128 // Length of the authentication tag in bits * }); - * console.log(new TextDecoder().decode(plaintext)); // 'Hello, world!' + * + * // Key Conversion + * const privateKeyBytes = await AesGcm.privateKeyToBytes({ privateKey }); * ``` */ export class AesGcm { + /** + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method accepts a symmetric key represented as a byte array (Uint8Array) and + * converts it into a JWK object for use with AES-GCM (Advanced Encryption Standard - + * Galois/Counter Mode). The conversion process involves encoding the key into + * base64url format and setting the appropriate JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'oct' for Octet Sequence (representing a symmetric key). + * - `k`: The symmetric key, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual symmetric key bytes + * const privateKey = await AesGcm.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKeyBytes - The raw symmetric key as a Uint8Array. + * + * @returns A Promise that resolves to the symmetric key in JWK format. + */ + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + k : Convert.uint8Array(privateKeyBytes).toBase64Url(), + kty : 'oct' + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + /** * Decrypts the provided data using AES-GCM. * + * This method performs AES-GCM decryption on the given encrypted data using the specified key. + * It requires an initialization vector (IV), the encrypted data along with the decryption key, + * and optionally, additional authenticated data (AAD). The method returns the decrypted data as a + * Uint8Array. The optional `tagLength` parameter specifies the size in bits of the authentication + * tag used when encrypting the data. If not specified, the default tag length of 128 bits is + * used. + * + * Example usage: + * + * ```ts + * const encryptedData = new Uint8Array([...]); // Encrypted data + * const iv = new Uint8Array([...]); // Initialization vector used during encryption + * const additionalData = new Uint8Array([...]); // Optional additional authenticated data + * const key = { ... }; // A PrivateKeyJwk object representing the AES key + * const decryptedData = await AesGcm.decrypt({ + * data: encryptedData, + * iv, + * additionalData, + * key, + * tagLength: 128 // Optional tag length in bits + * }); + * ``` + * * @param options - The options for the decryption operation. - * @param options.additionalData - Data that will be authenticated along with the encrypted data. - * @param options.data - The data to decrypt. - * @param options.iv - A unique initialization vector. - * @param options.key - The key to use for decryption. - * @param options.tagLength - This size of the authentication tag generated in bits. + * @param options.data - The encrypted data to decrypt, represented as a Uint8Array. + * @param options.iv - The initialization vector, represented as a Uint8Array. + * @param options.additionalData - Optional additional authenticated data. + * @param options.key - The key to use for decryption, represented in JWK format. + * @param options.tagLength - The length of the authentication tag in bits. + * * @returns A Promise that resolves to the decrypted data as a Uint8Array. */ public static async decrypt(options: { additionalData?: Uint8Array, data: Uint8Array, iv: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, tagLength?: number }): Promise { const { additionalData, data, iv, key, tagLength } = options; @@ -70,19 +159,43 @@ export class AesGcm { /** * Encrypts the provided data using AES-GCM. * + * This method performs AES-GCM encryption on the given data using the specified key. + * It requires an initialization vector (IV), the encrypted data along with the decryption key, + * and optionally, additional authenticated data (AAD). The method returns the encrypted data as a + * Uint8Array. The optional `tagLength` parameter specifies the size in bits of the authentication + * tag generated in the encryption operation and used for authentication in the corresponding + * decryption. If not specified, the default tag length of 128 bits is used. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); + * const iv = new Uint8Array([...]); // Initialization vector + * const additionalData = new Uint8Array([...]); // Optional additional authenticated data + * const key = { ... }; // A PrivateKeyJwk object representing an AES key + * const encryptedData = await AesGcm.encrypt({ + * data, + * iv, + * additionalData, + * key, + * tagLength: 128 // Optional tag length in bits + * }); + * ``` + * * @param options - The options for the encryption operation. - * @param options.additionalData - Data that will be authenticated along with the encrypted data. - * @param options.data - The data to decrypt. - * @param options.iv - A unique initialization vector. - * @param options.key - The key to use for decryption. - * @param options.tagLength - This size of the authentication tag generated in bits. + * @param options.data - The data to encrypt, represented as a Uint8Array. + * @param options.iv - The initialization vector, represented as a Uint8Array. + * @param options.additionalData - Optional additional authenticated data. + * @param options.key - The key to use for encryption, represented in JWK format. + * @param options.tagLength - The length of the authentication tag in bits. + * * @returns A Promise that resolves to the encrypted data as a Uint8Array. */ public static async encrypt(options: { additionalData?: Uint8Array, data: Uint8Array, iv: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, tagLength?: number }): Promise { const { additionalData, data, iv, key, tagLength } = options; @@ -103,36 +216,98 @@ export class AesGcm { } /** - * Generates an AES key of a given length. + * Generates a symmetric key for AES in Galois/Counter Mode (GCM) in JSON Web Key (JWK) format. + * + * This method creates a new symmetric key of a specified length suitable for use with + * AES-GCM encryption. It leverages cryptographically secure random number generation + * to ensure the uniqueness and security of the key. The generated key adheres to the JWK + * format, facilitating compatibility with common cryptographic standards and ease of use + * in various cryptographic applications. + * + * The generated key includes these components: + * - `kty`: Key Type, set to 'oct' for Octet Sequence, indicating a symmetric key. + * - `k`: The symmetric key component, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint, providing a unique identifier. * - * @param length - The length of the key in bits. - * @returns A Promise that resolves to the generated key as a Uint8Array. + * Example usage: + * + * ```ts + * const length = 256; // Length of the key in bits (e.g., 128, 192, 256) + * const privateKey = await AesGcm.generateKey({ length }); + * ``` + * + * @param options - The options for the key generation. + * @param options.length - The length of the key in bits. Common lengths are 128, 192, and 256 bits. + * + * @returns A Promise that resolves to the generated symmetric key in JWK format. */ public static async generateKey(options: { length: number - }): Promise { + }): Promise { const { length } = options; // Generate the secret key. const lengthInBytes = length / 8; - const secretKey = crypto.getRandomValues(new Uint8Array(lengthInBytes)); + const privateKeyBytes = crypto.getRandomValues(new Uint8Array(lengthInBytes)); + + // Convert private key from bytes to JWK format. + const privateKey = await AesGcm.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method takes a symmetric key in JWK format and extracts its raw byte representation. + * It focuses on the 'k' parameter of the JWK, which represents the symmetric key component + * in base64url encoding. The method decodes this value into a byte array, providing + * the symmetric key in its raw binary form. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // A symmetric key in JWK format + * const privateKeyBytes = await AesGcm.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKey - The symmetric key in JWK format. + * + * @returns A Promise that resolves to the symmetric key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Verify the provided JWK represents a valid oct private key. + if (!Jose.isOctPrivateKeyJwk(privateKey)) { + throw new Error(`AesGcm: The provided key is not a valid oct private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); - return secretKey; + return privateKeyBytes; } /** - * A private method to import a raw key for use with the Web Crypto API. + * A private method to import a key in JWK format for use with the Web Crypto API. * - * @param key - The raw key material. + * @param key - The key in JWK format. * @returns A Promise that resolves to a CryptoKey. */ - private static async importKey(key: Uint8Array): Promise { + private static async importKey(key: PrivateKeyJwk): Promise { return crypto.subtle.importKey( - 'raw', - key.buffer, - { name: 'AES-GCM', length: key.byteLength * 8 }, - true, - ['encrypt', 'decrypt'] + 'jwk', // format + key, // keyData + { name: 'AES-GCM' }, // algorithm + true, // extractable + ['encrypt', 'decrypt'] // usages ); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/ed25519.ts b/packages/crypto/src/crypto-primitives/ed25519.ts index 2ba767718..54fe83305 100644 --- a/packages/crypto/src/crypto-primitives/ed25519.ts +++ b/packages/crypto/src/crypto-primitives/ed25519.ts @@ -1,28 +1,33 @@ -import type { BytesKeyPair } from '../types/crypto-key.js'; +import { Convert } from '@web5/common'; +import { ed25519, edwardsToMontgomeryPub, edwardsToMontgomeryPriv, x25519 } from '@noble/curves/ed25519'; -import { ed25519, edwardsToMontgomeryPub, edwardsToMontgomeryPriv } from '@noble/curves/ed25519'; +import type { PrivateKeyJwk, PublicKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; /** - * The `Ed25519` class provides an interface for generating Ed25519 key pairs, - * computing public keys from private keys, and signing and verifying messages. + * The `Ed25519` class provides an interface for generating Ed25519 private + * keys, computing public keys from private keys, and signing and verifying + * messages. * * The class uses the '@noble/curves' package for the cryptographic operations. * - * The methods of this class are all asynchronous and return Promises. They all use - * the Uint8Array type for keys, signatures, and data, providing a consistent - * interface for working with binary data. + * The methods of this class are all asynchronous and return Promises. They all + * use the Uint8Array type for signatures and data, providing a + * consistent interface for working with binary data. * * Example usage: * * ```ts - * const keyPair = await Ed25519.generateKeyPair(); + * const privateKey = await Ed25519.generateKey(); * const message = new TextEncoder().encode('Hello, world!'); * const signature = await Ed25519.sign({ - * key: keyPair.privateKey, + * key: privateKey, * data: message * }); + * const publicKey = await Ed25519.getPublicKey({ privateKey }); * const isValid = await Ed25519.verify({ - * key: keyPair.publicKey, + * key: publicKey, * signature, * data: message * }); @@ -30,135 +35,463 @@ import { ed25519, edwardsToMontgomeryPub, edwardsToMontgomeryPriv } from '@noble * ``` */ export class Ed25519 { + /** + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method accepts a private key as a byte array (Uint8Array) for the Curve25519 curve in + * Twisted Edwards form and transforms it into a JWK object. The process involves first deriving + * the public key from the private key, then encoding both the private and public keys into + * base64url format. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'OKP' for Octet Key Pair. + * - `crv`: Curve Name, set to 'Ed25519'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The computed public key, base64url-encoded. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual private key bytes + * const privateKey = await Ed25519.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the private key conversion. + * @param options.privateKeyBytes - The raw private key as a Uint8Array. + * + * @returns A Promise that resolves to the private key in JWK format. + */ + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Derive the public key from the private key. + const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes); + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + crv : 'Ed25519', + d : Convert.uint8Array(privateKeyBytes).toBase64Url(), + kty : 'OKP', + x : Convert.uint8Array(publicKeyBytes).toBase64Url(), + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method accepts a public key as a byte array (Uint8Array) for the Curve25519 curve in + * Twisted Edwards form and transforms it into a JWK object. The process involves encoding the + * public key bytes into base64url format. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'OKP' for Octet Key Pair. + * - `crv`: Curve Name, set to 'X25519'. + * - `x`: The public key, base64url-encoded. + * + * Example usage: + * + * ```ts + * const publicKeyBytes = new Uint8Array([...]); // Replace with actual public key bytes + * const publicKey = await X25519.bytesToPublicKey({ publicKeyBytes }); + * ``` + * + * @param options - The options for the public key conversion. + * @param options.publicKeyBytes - The raw public key as a Uint8Array. + * + * @returns A Promise that resolves to the public key in JWK format. + */ + public static async bytesToPublicKey(options: { + publicKeyBytes: Uint8Array + }): Promise { + const { publicKeyBytes } = options; + + // Construct the public key in JWK format. + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : Convert.uint8Array(publicKeyBytes).toBase64Url(), + }; + + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await Jose.jwkThumbprint({ key: publicKey }); + + return publicKey; + } /** * Converts an Ed25519 private key to its X25519 counterpart. * - * Similar to the public key conversion, this method aids in transitioning - * from signing to encryption operations. By converting an Ed25519 private - * key to X25519 format, one can use the same key pair for both digital - * signatures and key exchange operations. + * This method enables the use of the same key pair for both digital signature (Ed25519) + * and key exchange (X25519) operations. It takes an Ed25519 private key and converts it + * to the corresponding X25519 format, facilitating interoperability between signing + * and encryption protocols. + * + * Example usage: + * + * ```ts + * const ed25519PrivateKey = { ... }; // An Ed25519 private key in JWK format + * const x25519PrivateKey = await Ed25519.convertPrivateKeyToX25519({ + * privateKey: ed25519PrivateKey + * }); + * ``` * * @param options - The options for the conversion. - * @param options.privateKey - The Ed25519 private key to convert, represented as a Uint8Array. - * @returns A Promise that resolves to the X25519 private key as a Uint8Array. + * @param options.privateKey - The Ed25519 private key to convert, in JWK format. + * + * @returns A Promise that resolves to the X25519 private key in JWK format. */ public static async convertPrivateKeyToX25519(options: { - privateKey: Uint8Array - }): Promise { + privateKey: PrivateKeyJwk + }): Promise { const { privateKey } = options; - // Converts Ed25519 private key to X25519 private key. - const montgomeryPrivateKey = edwardsToMontgomeryPriv(privateKey); + // Convert the provided Ed25519 private key to bytes. + const ed25519PrivateKeyBytes = await Ed25519.privateKeyToBytes({ privateKey }); + + // Convert the Ed25519 private key to an X25519 private key. + const x25519PrivateKeyBytes = edwardsToMontgomeryPriv(ed25519PrivateKeyBytes); - return montgomeryPrivateKey; + // Derive the X25519 public key from the X25519 private key. + const x25519PublicKeyBytes = x25519.getPublicKey(x25519PrivateKeyBytes); + + // Construct the X25519 private key in JWK format. + const x25519PrivateKey: PrivateKeyJwk = { + kty : 'OKP', + crv : 'X25519', + d : Convert.uint8Array(x25519PrivateKeyBytes).toBase64Url(), + x : Convert.uint8Array(x25519PublicKeyBytes).toBase64Url(), + }; + + // Compute the JWK thumbprint and set as the key ID. + x25519PrivateKey.kid = await Jose.jwkThumbprint({ key: x25519PrivateKey }); + + return x25519PrivateKey; } /** - * Converts an Ed25519 public key to its X25519 counterpart. - * - * This method is useful when transitioning from signing to encryption - * operations, as Ed25519 and X25519 keys share the same mathematical - * foundation but serve different purposes. Ed25519 keys are used for - * digital signatures, while X25519 keys are used for key exchange in - * encryption protocols like Diffie-Hellman. - * - * @param options - The options for the conversion. - * @param options.publicKey - The Ed25519 public key to convert, represented as a Uint8Array. - * @returns A Promise that resolves to the X25519 public key as a Uint8Array. - */ + * Converts an Ed25519 public key to its X25519 counterpart. + * + * This method enables the use of the same key pair for both digital signature (Ed25519) + * and key exchange (X25519) operations. It takes an Ed25519 public key and converts it + * to the corresponding X25519 format, facilitating interoperability between signing + * and encryption protocols. + * + * Example usage: + * + * ```ts + * const ed25519PublicKey = { ... }; // An Ed25519 public key in JWK format + * const x25519PublicKey = await Ed25519.convertPublicKeyToX25519({ + * publicKey: ed25519PublicKey + * }); + * + * @param options - The options for the conversion. + * @param options.publicKey - The Ed25519 public key to convert, in JWK format. + * + * @returns A Promise that resolves to the X25519 public key in JWK format. + */ public static async convertPublicKeyToX25519(options: { - publicKey: Uint8Array - }): Promise { + publicKey: PublicKeyJwk + }): Promise { const { publicKey } = options; + // Convert the provided private key to a byte array. + const ed25519PublicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); + // Verify Edwards public key is valid. - const isValid = await Ed25519.validatePublicKey({ key: publicKey }); + const isValid = await Ed25519.validatePublicKey({ key: ed25519PublicKeyBytes }); if (!isValid) { throw new Error('Ed25519: Invalid public key.'); } - // Converts Ed25519 public key to X25519 public key. - const montgomeryPublicKey = edwardsToMontgomeryPub(publicKey); + // Convert the Ed25519 public key to an X25519 private key. + const x25519PublicKeyBytes = edwardsToMontgomeryPub(ed25519PublicKeyBytes); - return montgomeryPublicKey; + // Construct the X25519 private key in JWK format. + const x25519PublicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'X25519', + x : Convert.uint8Array(x25519PublicKeyBytes).toBase64Url(), + }; + + // Compute the JWK thumbprint and set as the key ID. + x25519PublicKey.kid = await Jose.jwkThumbprint({ key: x25519PublicKey }); + + return x25519PublicKey; } /** - * Generates an Ed25519 key pair. + * Derives the public key in JWK format from a given Ed25519 private key. + * + * This method takes a private key in JWK format and derives its corresponding public key, + * also in JWK format. The derivation process involves converting the private key to a + * raw byte array and then computing the corresponding public key on the Curve25519 curve in + * Twisted Edwards form. The public key is then encoded into base64url format to construct + * a JWK representation. * - * @returns A Promise that resolves to an object containing the private and public keys as Uint8Array. + * Example usage: + * + * ```ts + * const privateKey = { ... }; // A PrivateKeyJwk object representing an Ed25519 private key + * const publicKey = await Ed25519.computePublicKey({ privateKey }); + * ``` + * + * @param options - The options for the public key derivation. + * @param options.privateKey - The private key in JWK format from which to derive the public key. + * + * @returns A Promise that resolves to the computed public key in JWK format. */ - public static async generateKeyPair(): Promise { - // Generate the private key and compute its public key. - const privateKey = ed25519.utils.randomPrivateKey(); - const publicKey = ed25519.getPublicKey(privateKey); - - const keyPair = { - privateKey : privateKey, - publicKey : publicKey + public static async computePublicKey(options: { + privateKey: PrivateKeyJwk + }): Promise { + let { privateKey } = options; + + // Convert the provided private key to a byte array. + const privateKeyBytes = await Ed25519.privateKeyToBytes({ privateKey }); + + // Derive the public key from the private key. + const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes); + + // Construct the public key in JWK format. + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : Convert.uint8Array(publicKeyBytes).toBase64Url() }; - return keyPair; + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await Jose.jwkThumbprint({ key: publicKey }); + + return publicKey; } /** - * Computes the public key from a given private key. + * Generates an Ed25519 private key in JSON Web Key (JWK) format. * - * @param options - The options for the public key computation. - * @param options.privateKey - The 32-byte private key from which to compute the public key. - * @returns A Promise that resolves to the computed 32-byte public key as a Uint8Array. + * This method creates a new private key suitable for use with the Curve25519 elliptic curve in + * Twisted Edwards form. The key generation process involves using cryptographically secure + * random number generation to ensure the uniqueness and security of the key. The resulting + * private key adheres to the JWK format making it compatible with common cryptographic + * standards and easy to use in various cryptographic processes. + * + * The generated private key in JWK format includes the following components: + * - `kty`: Key Type, set to 'OKP' for Octet Key Pair. + * - `crv`: Curve Name, set to 'Ed25519'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The derived public key, base64url-encoded. + * + * Example usage: + * + * ```ts + * const privateKey = await X25519.generateKey(); + * ``` + * + * @returns A Promise that resolves to the generated private key in JWK format. */ - public static async getPublicKey(options: { - privateKey: Uint8Array + public static async generateKey(): Promise { + // Generate a random private key. + const privateKeyBytes = ed25519.utils.randomPrivateKey(); + + // Convert private key from bytes to JWK format. + const privateKey = await Ed25519.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method accepts a private key in JWK format and extracts its raw byte representation. + * + * This method accepts a public key in JWK format and converts it into its raw binary + * form. The conversion process involves decoding the 'd' parameter of the JWK + * from base64url format into a byte array. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // An Ed25519 private key in JWK format + * const privateKeyBytes = await Ed25519.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the private key conversion. + * @param options.privateKey - The private key in JWK format. + * + * @returns A Promise that resolves to the private key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk }): Promise { - let { privateKey } = options; + const { privateKey } = options; - // Compute public key. - const publicKey = ed25519.getPublicKey(privateKey); + // Verify the provided JWK represents a valid OKP private key. + if (!Jose.isOkpPrivateKeyJwk(privateKey)) { + throw new Error(`Ed25519: The provided key is not a valid OKP private key.`); + } - return publicKey; + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + + return privateKeyBytes; + } + + /** + * Converts a public key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method accepts a public key in JWK format and converts it into its raw binary form. + * The conversion process involves decoding the 'x' parameter of the JWK (which represent the + * x coordinate of the elliptic curve point) from base64url format into a byte array. + * + * Example usage: + * + * ```ts + * const publicKey = { ... }; // An Ed25519 public key in JWK format + * const publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); + * ``` + * + * @param options - The options for the public key conversion. + * @param options.publicKey - The public key in JWK format. + * + * @returns A Promise that resolves to the public key as a Uint8Array. + */ + public static async publicKeyToBytes(options: { + publicKey: PublicKeyJwk + }): Promise { + const { publicKey } = options; + + // Verify the provided JWK represents a valid OKP public key. + if (!Jose.isOkpPublicKeyJwk(publicKey)) { + throw new Error(`Ed25519: The provided key is not a valid OKP public key.`); + } + + // Decode the provided public key to bytes. + const publicKeyBytes = Convert.base64Url(publicKey.x).toUint8Array(); + + return publicKeyBytes; } /** - * Generates a RFC8032 EdDSA signature of given data with a given private key. + * Generates an RFC8032-compliant EdDSA signature of given data using an Ed25519 private key. + * + * This method signs the provided data with a specified private key using the EdDSA + * (Edwards-curve Digital Signature Algorithm) as defined in RFC8032. It + * involves converting the private key from JWK format to a byte array and then employing + * the Ed25519 algorithm to sign the data. The output is a digital signature in the form + * of a Uint8Array, uniquely corresponding to both the data and the private key used for + * signing. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); // Data to be signed + * const privateKey = { ... }; // A PrivateKeyJwk object representing an Ed25519 private key + * const signature = await Ed25519.sign({ + * data, + * key: privateKey + * }); + * ``` * * @param options - The options for the signing operation. - * @param options.key - The private key to use for signing. - * @param options.data - The data to sign. + * @param options.data - The data to sign, represented as a Uint8Array. + * @param options.key - The private key to use for signing, represented in JWK format. + * * @returns A Promise that resolves to the signature as a Uint8Array. */ public static async sign(options: { data: Uint8Array, - key: Uint8Array + key: PrivateKeyJwk }): Promise { const { key, data } = options; - // Signature operation. - const signature = ed25519.sign(data, key); + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await Ed25519.privateKeyToBytes({ privateKey: key }); + + // Sign the provided data using the EdDSA algorithm. + const signature = ed25519.sign(data, privateKeyBytes); return signature; } /** - * Validates a given public key to ensure that it corresponds to a - * valid point on the Ed25519 elliptic curve. + * Verifies an RFC8032-compliant EdDSA signature against given data using an Ed25519 public key. + * + * This method validates a digital signature to ensure its authenticity and integrity. + * It uses the EdDSA (Edwards-curve Digital Signature Algorithm) as specified in RFC8032. + * The verification process involves converting the public key from JWK format to a raw + * byte array and using the Ed25519 algorithm to validate the signature against the provided data. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); // Data that was signed + * const publicKey = { ... }; // A PublicKeyJwk object representing an Ed25519 public key + * const signature = new Uint8Array([...]); // Signature to verify + * const isValid = await Ed25519.verify({ + * data, + * key: publicKey, + * signature + * }); + * console.log(isValid); // true if the signature is valid, false otherwise + * ``` + * + * @param options - The options for the verification operation. + * @param options.data - The data that was signed, represented as a Uint8Array. + * @param options.key - The public key in JWK format used for verification. + * @param options.signature - The signature to verify, represented as a Uint8Array. + * + * @returns A Promise that resolves to a boolean indicating whether the signature is valid. + */ + public static async verify(options: { + data: Uint8Array, + key: PublicKeyJwk, + signature: Uint8Array + }): Promise { + const { key, signature, data } = options; + + // Convert the public key from JWK format to bytes. + const publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey: key }); + + // Perform the verification of the signature. + const isValid = ed25519.verify(signature, data, publicKeyBytes); + + return isValid; + } + + /** + * Validates a given public key to confirm its mathematical correctness on the Edwards curve. * - * This method decodes the Edwards points from the key bytes and - * asserts their validity on the curve. If the points are not valid, - * the method returns false. If the points are valid, the method - * returns true. + * This method decodes the Edwards points from the key bytes and asserts their validity on the + * Curve25519 curve in Twisted Edwards form. If the points are not valid, the method returns + * false. If the points are valid, the method returns true. * - * Note: This method does not check whether the key corresponds to a - * known or authorized entity, or whether it has been compromised. - * It only checks the mathematical validity of the key. + * Note that this validation strictly pertains to the key's format and numerical validity; it does + * not assess whether the key corresponds to a known entity or its security status (e.g., whether + * it has been compromised). + * + * Example usage: + * + * ```ts + * const publicKey = new Uint8Array([...]); // A public key in byte format + * const isValid = await Secp256k1.validatePublicKey({ key: publicKey }); + * console.log(isValid); // true if the key is valid on the Edwards curve, false otherwise + * ``` + * + * @param options - The options for the public key validation. + * @param options.key - The public key to validate, represented as a Uint8Array. * - * @param options - The options for the key validation. - * @param options.key - The key to validate, represented as a Uint8Array. * @returns A Promise that resolves to a boolean indicating whether the key * corresponds to a valid point on the Edwards curve. */ - public static async validatePublicKey(options: { + private static async validatePublicKey(options: { key: Uint8Array }): Promise { const { key } = options; @@ -176,26 +509,4 @@ export class Ed25519 { return true; } - - /** - * Verifies a RFC8032 EdDSA signature of given data with a given public key. - * - * @param options - The options for the verification operation. - * @param options.key - The public key to use for verification. - * @param options.signature - The signature to verify. - * @param options.data - The data that was signed. - * @returns A Promise that resolves to a boolean indicating whether the signature is valid. - */ - public static async verify(options: { - data: Uint8Array, - key: Uint8Array, - signature: Uint8Array - }): Promise { - const { key, signature, data } = options; - - // Verify operation. - const isValid = ed25519.verify(signature, data, key); - - return isValid; - } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/pbkdf2.ts b/packages/crypto/src/crypto-primitives/pbkdf2.ts index d10e60cc6..fe7e15061 100644 --- a/packages/crypto/src/crypto-primitives/pbkdf2.ts +++ b/packages/crypto/src/crypto-primitives/pbkdf2.ts @@ -10,7 +10,69 @@ type DeriveKeyOptions = { length: number }; +/** + * The `Pbkdf2` class provides a secure way to derive cryptographic keys from a password + * using the PBKDF2 (Password-Based Key Derivation Function 2) algorithm. This class + * supports both the Web Crypto API and Node.js Crypto module to offer flexibility in + * different JavaScript environments. + * + * The PBKDF2 algorithm is widely used for generating keys from passwords, as it applies + * a pseudorandom function to the input password along with a salt value and iterates the + * process multiple times to increase the key's resistance to brute-force attacks. + * + * This class offers a single static method `deriveKey` to perform key derivation. It + * automatically chooses between Web Crypto and Node.js Crypto based on the runtime + * environment's support. + * + * Usage Examples: + * + * ```ts + * const options = { + * hash: 'SHA-256', // The hash function to use ('SHA-256', 'SHA-384', 'SHA-512') + * password: new TextEncoder().encode('password'), // The password as a Uint8Array + * salt: new Uint8Array([...]), // The salt value + * iterations: 1000, // The number of iterations + * length: 256 // The length of the derived key in bits + * }; + * const derivedKey = await Pbkdf2.deriveKey(options); + * ``` + * + * @remarks + * This class relies on the availability of the Web Crypto API or Node.js Crypto module. + * It falls back to Node.js Crypto if Web Crypto is not supported in the environment. + */ export class Pbkdf2 { + /** + * Derives a cryptographic key from a password using the PBKDF2 algorithm. + * + * This method applies the PBKDF2 algorithm to the provided password along with + * a salt value and iterates the process a specified number of times. It uses + * a cryptographic hash function to enhance security and produce a key of the + * desired length. The method is capable of utilizing either the Web Crypto API + * or the Node.js Crypto module, depending on the environment's support. + * + * Example usage: + * + * ```ts + * const options = { + * hash: 'SHA-256', + * password: new TextEncoder().encode('password'), + * salt: new Uint8Array([...]), + * iterations: 1000, + * length: 256 + * }; + * const derivedKey = await Pbkdf2.deriveKey(options); + * + * @param options - The options for key derivation. + * @param options.hash - The hash function to use, such as 'SHA-256', 'SHA-384', or 'SHA-512'. + * @param options.password - The password from which to derive the key, represented as a Uint8Array. + * @param options.salt - The salt value to use in the derivation process, as a Uint8Array. + * @param options.iterations - The number of iterations to apply in the PBKDF2 algorithm. + * @param options.length - The desired length of the derived key in bits. + * + * @returns A Promise that resolves to the derived key as a Uint8Array. + * ``` + */ public static async deriveKey(options: DeriveKeyOptions): Promise { if (isWebCryptoSupported()) { return Pbkdf2.deriveKeyWithWebCrypto(options); diff --git a/packages/crypto/src/crypto-primitives/secp256k1.ts b/packages/crypto/src/crypto-primitives/secp256k1.ts index a958210bb..79ffd16c9 100644 --- a/packages/crypto/src/crypto-primitives/secp256k1.ts +++ b/packages/crypto/src/crypto-primitives/secp256k1.ts @@ -1,180 +1,440 @@ -import type { BytesKeyPair } from '../types/crypto-key.js'; - +import { Convert } from '@web5/common'; import { sha256 } from '@noble/hashes/sha256'; import { secp256k1 } from '@noble/curves/secp256k1'; import { numberToBytesBE } from '@noble/curves/abstract/utils'; -export type HashFunction = (data: Uint8Array) => Uint8Array; +import type { PrivateKeyJwk, PublicKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; /** - * The `Secp256k1` class provides an interface for generating secp256k1 key pairs, - * computing public keys from private keys, generating shaerd secrets, and - * signing and verifying messages. + * The `Secp256k1` class provides a comprehensive suite of utilities for working with + * the secp256k1 elliptic curve, commonly used in blockchain and cryptographic applications. + * This class includes methods for key generation, conversion, signing, verification, and + * Elliptic Curve Diffie-Hellman (ECDH) key agreement, all compliant with relevant cryptographic + * standards. * - * The class uses the '@noble/secp256k1' package for the cryptographic operations, - * and the '@noble/hashes/sha256' package for generating the hash digests needed - * for the signing and verification operations. + * The class supports conversions between raw byte formats and JSON Web Key (JWK) formats, + * making it versatile for various cryptographic tasks. It adheres to RFC6090 for ECDH and + * RFC6979 for ECDSA signing and verification, ensuring compatibility and security. * - * The methods of this class are all asynchronous and return Promises. They all use - * the Uint8Array type for keys, signatures, and data, providing a consistent - * interface for working with binary data. + * Key Features: + * - Key Generation: Generate secp256k1 private keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Public Key Derivation: Derive public keys from private keys. + * - ECDH Shared Secret Computation: Securely derive shared secrets using private and public keys. + * - ECDSA Signing and Verification: Sign data and verify signatures with secp256k1 keys. + * - Key Validation: Validate the mathematical correctness of secp256k1 keys. * - * Example usage: + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments. + * + * Usage Examples: * * ```ts - * const keyPair = await Secp256k1.generateKeyPair(); - * const message = new TextEncoder().encode('Hello, world!'); + * // Key Generation + * const privateKey = await Secp256k1.generateKey(); + * + * // Public Key Derivation + * const publicKey = await Secp256k1.computePublicKey({ privateKey }); + * + * // ECDH Shared Secret Computation + * const sharedSecret = await Secp256k1.sharedSecret({ + * privateKeyA: privateKey, + * publicKeyB: anotherPublicKey + * }); + * + * // ECDSA Signing * const signature = await Secp256k1.sign({ - * algorithm: { hash: 'SHA-256' }, - * key: keyPair.privateKey, - * data: message + * data: new TextEncoder().encode('Message'), + * key: privateKey * }); + * + * // ECDSA Signature Verification * const isValid = await Secp256k1.verify({ - * algorithm: { hash: 'SHA-256' }, - * key: keyPair.publicKey, - * signature, - * data: message + * data: new TextEncoder().encode('Message'), + * key: publicKey, + * signature: signature + * }); + * + * // Key Conversion + * const publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey }); + * const privateKeyBytes = await Secp256k1.privateKeyToBytes({ privateKey }); + * const compressedPublicKey = await Secp256k1.convertPublicKey({ + * publicKey: publicKeyBytes, + * compressedPublicKey: true * }); - * console.log(isValid); // true + * const uncompressedPublicKey = await Secp256k1.convertPublicKey({ + * publicKey: publicKeyBytes, + * compressedPublicKey: false + * }); + * + * // Key Validation + * const isPrivateKeyValid = await Secp256k1.validatePrivateKey({ key: privateKeyBytes }); + * const isPublicKeyValid = await Secp256k1.validatePublicKey({ key: publicKeyBytes }); * ``` */ export class Secp256k1 { /** - * A private static field containing a map of hash algorithm names to their - * corresponding hash functions. The map is used in the 'sign' and 'verify' - * methods to get the specified hash function. + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method takes a private key represented as a byte array (Uint8Array) and + * converts it into a JWK object. The conversion involves extracting the + * elliptic curve points (x and y coordinates) from the private key and encoding + * them into base64url format, alongside other JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'EC' for Elliptic Curve. + * - `crv`: Curve Name, set to 'secp256k1'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The x-coordinate of the public key point, base64url-encoded. + * - `y`: The y-coordinate of the public key point, base64url-encoded. + * + * This method is useful for converting raw public keys into a standardized + * JSON format, facilitating their use in cryptographic operations and making + * them easy to share and store. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual private key bytes + * const privateKey = await Secp256k1.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the private key conversion. + * @param options.privateKeyBytes - The raw private key as a Uint8Array. + * + * @returns A Promise that resolves to the private key in JWK format. */ - private static hashAlgorithms: Record = { - 'SHA-256': sha256 - }; + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Get the elliptic curve points (x and y coordinates) for the provided private key. + const points = await Secp256k1.getCurvePoints({ key: privateKeyBytes }); + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + d : Convert.uint8Array(privateKeyBytes).toBase64Url(), + x : Convert.uint8Array(points.x).toBase64Url(), + y : Convert.uint8Array(points.y).toBase64Url() + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } /** - * Converts a public key between its compressed and uncompressed forms. + * Converts a raw public key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method accepts a public key in a byte array (Uint8Array) format and + * transforms it to a JWK object. It involves decoding the elliptic curve points + * (x and y coordinates) from the raw public key bytes and encoding them into + * base64url format, along with setting appropriate JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'EC' for Elliptic Curve. + * - `crv`: Curve Name, set to 'secp256k1'. + * - `x`: The x-coordinate of the public key point, base64url-encoded. + * - `y`: The y-coordinate of the public key point, base64url-encoded. * - * Given a public key, this method can either compress or decompress it - * depending on the provided `compressedPublicKey` option. The conversion - * process involves decoding the Weierstrass points from the key bytes - * and then returning the key in the desired format. + * This method is useful for converting raw public keys into a standardized + * JSON format, facilitating their use in cryptographic operations and making + * them easy to share and store. * - * This is useful in scenarios where space is a consideration or when - * interfacing with systems that expect a specific public key format. + * Example usage: + * + * ```ts + * const publicKeyBytes = new Uint8Array([...]); // Replace with actual public key bytes + * const publicKey = await Secp256k1.bytesToPublicKey({ publicKeyBytes }); + * ``` * * @param options - The options for the public key conversion. - * @param options.publicKey - The original public key, represented as a Uint8Array. - * @param options.compressedPublicKey - A boolean indicating whether the output - * should be in compressed form. If true, the - * method returns the compressed form of the - * provided public key. If false, it returns - * the uncompressed form. - * - * @returns A Promise that resolves to the converted public key as a Uint8Array. + * @param options.publicKeyBytes - The raw public key as a Uint8Array. + * + * @returns A Promise that resolves to the public key in JWK format. + */ + public static async bytesToPublicKey(options: { + publicKeyBytes: Uint8Array + }): Promise { + const { publicKeyBytes } = options; + + // Get the elliptic curve points (x and y coordinates) for the provided public key. + const points = await Secp256k1.getCurvePoints({ key: publicKeyBytes }); + + // Construct the public key in JWK format. + const publicKey: PublicKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : Convert.uint8Array(points.x).toBase64Url(), + y : Convert.uint8Array(points.y).toBase64Url() + }; + + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await Jose.jwkThumbprint({ key: publicKey }); + + return publicKey; + } + + /** + * Converts a public key to its compressed form. + * + * This method takes a public key represented as a byte array and compresses it. Public key + * compression is a process that reduces the size of the public key by removing the y-coordinate, + * making it more efficient for storage and transmission. The compressed key retains the same + * level of security as the uncompressed key. + * + * Example usage: + * + * ```ts + * const uncompressedPublicKeyBytes = new Uint8Array([...]); // Replace with actual uncompressed public key bytes + * const compressedPublicKey = await Secp256k1.compressPublicKey({ + * publicKeyBytes: uncompressedPublicKeyBytes + * }); + * ``` + * + * @param options - The options for the public key compression. + * @param options.publicKeyBytes - The public key as a Uint8Array. + * + * @returns A Promise that resolves to the compressed public key as a Uint8Array. */ - public static async convertPublicKey(options: { - publicKey: Uint8Array, - compressedPublicKey: boolean + public static async compressPublicKey(options: { + publicKeyBytes: Uint8Array }): Promise { - let { publicKey, compressedPublicKey } = options; + let { publicKeyBytes } = options; - // Decode Weierstrass points from key bytes. - const point = secp256k1.ProjectivePoint.fromHex(publicKey); + // Decode Weierstrass points from the public key byte array. + const point = secp256k1.ProjectivePoint.fromHex(publicKeyBytes); - // Return either the compressed or uncompressed form of hte public key. - return point.toRawBytes(compressedPublicKey); + // Return the compressed form of the public key. + return point.toRawBytes(true); } /** - * Generates a secp256k1 key pair. + * Converts a public key to its uncompressed form. + * + * This method takes a compressed public key represented as a byte array and decompresses it. + * Public key decompression involves reconstructing the y-coordinate from the x-coordinate, + * resulting in the full public key. This method is used when the uncompressed key format is + * required for certain cryptographic operations or interoperability. + * + * Example usage: * - * @param options - Optional parameters for the key generation. - * @param options.compressedPublicKey - If true, generates a compressed public key. Defaults to true. - * @returns A Promise that resolves to an object containing the private and public keys as Uint8Array. + * ```ts + * const compressedPublicKeyBytes = new Uint8Array([...]); // Replace with actual compressed public key bytes + * const decompressedPublicKey = await Secp256k1.decompressPublicKey({ + * publicKeyBytes: compressedPublicKeyBytes + * }); + * ``` + * + * @param options - The options for the public key decompression. + * @param options.publicKeyBytes - The public key as a Uint8Array. + * + * @returns A Promise that resolves to the uncompressed public key as a Uint8Array. */ - public static async generateKeyPair(options?: { - compressedPublicKey?: boolean - }): Promise { - let { compressedPublicKey } = options ?? { }; + public static async decompressPublicKey(options: { + publicKeyBytes: Uint8Array + }): Promise { + let { publicKeyBytes } = options; - compressedPublicKey ??= true; // Default to compressed public key, matching the default of @noble/secp256k1. + // Decode Weierstrass points from the public key byte array. + const point = secp256k1.ProjectivePoint.fromHex(publicKeyBytes); - // Generate the private key and compute its public key. - const privateKey = secp256k1.utils.randomPrivateKey(); - const publicKey = secp256k1.getPublicKey(privateKey, compressedPublicKey); + // Return the uncompressed form of the public key. + return point.toRawBytes(false); + } - const keyPair = { - privateKey : privateKey, - publicKey : publicKey + /** + * Derives the public key in JWK format from a given private key. + * + * This method takes a private key in JWK format and derives its corresponding public key, + * also in JWK format. The derivation process involves converting the private key to a raw + * byte array, then computing the elliptic curve points (x and y coordinates) from this private + * key. These coordinates are then encoded into base64url format to construct the public key in + * JWK format. + * + * The process ensures that the derived public key correctly corresponds to the given private key, + * adhering to the secp256k1 elliptic curve standards. This method is useful in cryptographic + * operations where a public key is needed for operations like signature verification, but only + * the private key is available. + * + * Example usage: + * + * ```ts + * const privateKeyJwk = { ... }; // A PrivateKeyJwk object representing a secp256k1 private key + * const publicKeyJwk = await Secp256k1.computePublicKey({ privateKey: privateKeyJwk }); + * ``` + * + * @param options - The options for the public key derivation. + * @param options.privateKey - The private key in JWK format from which to derive the public key. + * + * @returns A Promise that resolves to the derived public key in JWK format. + */ + public static async computePublicKey(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Convert the provided private key to a byte array. + const privateKeyBytes = await Secp256k1.privateKeyToBytes({ privateKey }); + + // Get the elliptic curve points (x and y coordinates) for the provided private key. + const points = await Secp256k1.getCurvePoints({ key: privateKeyBytes }); + + // Construct the public key in JWK format. + const publicKey: PublicKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : Convert.uint8Array(points.x).toBase64Url(), + y : Convert.uint8Array(points.y).toBase64Url() }; - return keyPair; + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await Jose.jwkThumbprint({ key: publicKey }); + + return publicKey; } /** - * Returns the elliptic curve points (x and y coordinates) for a given secp256k1 key. + * Generates a secp256k1 private key in JSON Web Key (JWK) format. * - * In the case of a private key, the public key is first computed from the private key, - * then the x and y coordinates of the public key point on the elliptic curve are returned. + * This method creates a new private key suitable for use with the secp256k1 + * elliptic curve. The key is generated using cryptographically secure random + * number generation to ensure its uniqueness and security. The resulting + * private key adheres to the JWK format, specifically tailored for secp256k1, + * making it compatible with common cryptographic standards and easy to use in + * various cryptographic processes. * - * In the case of a public key, the x and y coordinates of the key point on the elliptic - * curve are returned directly. + * The private key generated by this method includes the following components: + * - `kty`: Key Type, set to 'EC' for Elliptic Curve. + * - `crv`: Curve Name, set to 'secp256k1'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The x-coordinate of the public key point, derived from the private key, base64url-encoded. + * - `y`: The y-coordinate of the public key point, derived from the private key, base64url-encoded. * - * The returned coordinates can be used to perform various operations on the elliptic curve, - * such as addition and multiplication of points, which can be used in various cryptographic - * schemes and protocols. + * The key is returned in a format suitable for direct use in signin and key agreement operations. * - * @param options - The options for the operation. - * @param options.key - The key for which to get the elliptic curve points. - * Can be either a private key or a public key. - * The key should be passed as a Uint8Array. - * @returns A Promise that resolves to an object with properties 'x' and 'y', - * each being a Uint8Array representing the x and y coordinates of the key point on the elliptic curve. + * Example usage: + * + * ```ts + * const privateKey = await Secp256k1.generateKey(); + * ``` + * + * @returns A Promise that resolves to the generated private key in JWK format. */ - public static async getCurvePoints(options: { - key: Uint8Array - }): Promise<{ x: Uint8Array, y: Uint8Array }> { - let { key } = options; + public static async generateKey(): Promise { + // Generate a random private key. + const privateKeyBytes = secp256k1.utils.randomPrivateKey(); - // If key is a private key, first compute the public key. - if (key.byteLength === 32) { - key = await Secp256k1.getPublicKey({ privateKey: key }); - } + // Convert private key from bytes to JWK format. + const privateKey = await Secp256k1.bytesToPrivateKey({ privateKeyBytes }); - // Decode Weierstrass points from key bytes. - const point = secp256k1.ProjectivePoint.fromHex(key); + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); - // Get x- and y-coordinate values and convert to Uint8Array. - const x = numberToBytesBE(point.x, 32); - const y = numberToBytesBE(point.y, 32); + return privateKey; + } - return { x, y }; + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method takes a private key in JWK format and extracts its raw byte representation. + * It specifically focuses on the 'd' parameter of the JWK, which represents the private + * key component in base64url encoding. The method decodes this value into a byte array. + * + * This conversion is essential for operations that require the private key in its raw + * binary form, such as certain low-level cryptographic operations or when interfacing + * with systems and libraries that expect keys in a byte array format. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // An X25519 private key in JWK format + * const privateKeyBytes = await Secp256k1.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the private key conversion. + * @param options.privateKey - The private key in JWK format. + * + * @returns A Promise that resolves to the private key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Verify the provided JWK represents a valid EC secp256k1 private key. + if (!Jose.isEcPrivateKeyJwk(privateKey)) { + throw new Error(`Secp256k1: The provided key is not a valid EC private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + + return privateKeyBytes; } /** - * Computes the public key from a given private key. - * If compressedPublicKey=true then the output is a 33-byte public key. - * If compressedPublicKey=false then the output is a 65-byte public key. - * - * @param options - The options for the public key computation. - * @param options.privateKey - The 32-byte private key from which to compute the public key. - * @param options.compressedPublicKey - If true, returns a compressed public key. Defaults to true. - * @returns A Promise that resolves to the computed public key as a Uint8Array. + * Converts a public key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method accepts a public key in JWK format and converts it into its raw binary + * form. The conversion process involves decoding the 'x' and 'y' parameters of the JWK + * (which represent the x and y coordinates of the elliptic curve point, respectively) + * from base64url format into a byte array. The method then concatenates these values, + * along with a prefix indicating the key format, to form the full public key. + * + * This function is particularly useful for use cases where the public key is needed + * in its raw byte format, such as for certain cryptographic operations or when + * interfacing with systems that require raw key formats. + * + * Example usage: + * + * ```ts + * const publicKey = { ... }; // A PublicKeyJwk object + * const publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey }); + * ``` + * + * @param options - The options for the public key conversion. + * @param options.publicKey - The public key in JWK format. + * + * @returns A Promise that resolves to the public key as a Uint8Array. */ - public static async getPublicKey(options: { - privateKey: Uint8Array, - compressedPublicKey?: boolean + public static async publicKeyToBytes(options: { + publicKey: PublicKeyJwk }): Promise { - let { privateKey, compressedPublicKey } = options; + const { publicKey } = options; + + // Verify the provided JWK represents a valid EC secp256k1 public key, which must have a 'y' value. + if (!(Jose.isEcPublicKeyJwk(publicKey) && publicKey.y)) { + throw new Error(`Secp256k1: The provided key is not a valid EC public key.`); + } - compressedPublicKey ??= true; // Default to compressed public key, matching the default of @noble/secp256k1. + // Decode the provided public key to bytes. + const prefix = new Uint8Array([0x04]); // Designates an uncompressed key. + const x = Convert.base64Url(publicKey.x).toUint8Array(); + const y = Convert.base64Url(publicKey.y).toUint8Array(); - // Compute public key. - const publicKey = secp256k1.getPublicKey(privateKey, compressedPublicKey); + // Concatenate the prefix, x-coordinate, and y-coordinate as a single byte array. + const publicKeyBytes = new Uint8Array([...prefix, ...x, ...y]); - return publicKey; + return publicKeyBytes; } /** - * Generates a RFC6090 ECDH shared secret given the private key of one party - * and the public key another party. + * Computes an RFC6090-compliant Elliptic Curve Diffie-Hellman (ECDH) shared secret + * using secp256k1 private and public keys in JSON Web Key (JWK) format. + * + * This method facilitates the ECDH key agreement protocol, which is a method of securely + * deriving a shared secret between two parties based on their private and public keys. + * It takes the private key of one party (privateKeyA) and the public key of another + * party (publicKeyB) to compute a shared secret. The shared secret is derived from the + * x-coordinate of the elliptic curve point resulting from the multiplication of the + * public key with the private key. * * Note: When performing Elliptic Curve Diffie-Hellman (ECDH) key agreement, * the resulting shared secret is a point on the elliptic curve, which @@ -183,18 +443,43 @@ export class Secp256k1 { * in the ECDH process, it's standard practice to use only the x-coordinate * of the shared secret point as the resulting shared key. This is because * the y-coordinate does not add to the entropy of the key, and both parties - * can independently compute the x-coordinate, so using just the x-coordinate - * simplifies matters. + * can independently compute the x-coordinate. Consquently, this implementation + * omits the y-coordinate for simplicity and standard compliance. + * + * Example usage: + * + * ```ts + * const privateKeyA = { ... }; // A PrivateKeyJwk object for party A + * const publicKeyB = { ... }; // A PublicKeyJwk object for party B + * const sharedSecret = await Secp256k1.sharedSecret({ + * privateKeyA, + * publicKeyB + * }); + * ``` + * + * @param options - The options for the shared secret computation operation. + * @param options.privateKeyA - The private key in JWK format of one party. + * @param options.publicKeyB - The public key in JWK format of the other party. + * + * @returns A Promise that resolves to the computed shared secret as a Uint8Array. */ public static async sharedSecret(options: { - compressedSecret?: boolean, - privateKey: Uint8Array, - publicKey: Uint8Array + privateKeyA: PrivateKeyJwk, + publicKeyB: PublicKeyJwk }): Promise { - let { privateKey, publicKey } = options; + let { privateKeyA, publicKeyB } = options; + + // Ensure that keys from the same key pair are not specified. + if ('x' in privateKeyA && 'x' in publicKeyB && privateKeyA.x === publicKeyB.x) { + throw new Error(`Secp256k1: ECDH shared secret cannot be computed from a single key pair's public and private keys.`); + } + + // Convert the provided private and public keys to bytes. + const privateKeyABytes = await Secp256k1.privateKeyToBytes({ privateKey: privateKeyA }); + const publicKeyBBytes = await Secp256k1.publicKeyToBytes({ publicKey: publicKeyB }); - // Compute the shared secret between the public and private keys. - const sharedSecret = secp256k1.getSharedSecret(privateKey, publicKey); + // Compute the compact representation shared secret between the public and private keys. + const sharedSecret = secp256k1.getSharedSecret(privateKeyABytes, publicKeyBBytes, true); // Remove the leading byte that indicates the sign of the y-coordinate // of the point on the elliptic curve. See note above. @@ -202,49 +487,196 @@ export class Secp256k1 { } /** - * Generates a RFC6979 ECDSA signature of given data with a given private key and hash algorithm. + * Generates an RFC6979-compliant ECDSA signature of given data using a secp256k1 private key. + * + * This method signs the provided data with a specified private key using the ECDSA + * (Elliptic Curve Digital Signature Algorithm) signature algorithm, as defined in RFC6979. + * The data to be signed is first hashed using the SHA-256 algorithm, and this hash is then + * signed using the private key. The output is a digital signature in the form of a + * Uint8Array, which uniquely corresponds to both the data and the private key used for signing. + * + * This method is commonly used in cryptographic applications to ensure data integrity and + * authenticity. The signature can later be verified by parties with access to the corresponding + * public key, ensuring that the data has not been tampered with and was indeed signed by the + * holder of the private key. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); // Data to be signed + * const privateKey = { ... }; // A PrivateKeyJwk object representing a secp256k1 private key + * const signature = await Secp256k1.sign({ + * data, + * key: privateKey + * }); + * ``` * * @param options - The options for the signing operation. - * @param options.data - The data to sign. - * @param options.hash - The hash algorithm to use to generate a digest of the data. - * @param options.key - The private key to use for signing. + * @param options.data - The data to sign, represented as a Uint8Array. + * @param options.key - The private key to use for signing, represented in JWK format. + * * @returns A Promise that resolves to the signature as a Uint8Array. */ public static async sign(options: { data: Uint8Array, - hash: string, - key: Uint8Array + key: PrivateKeyJwk }): Promise { - const { data, hash, key } = options; + const { data, key } = options; + + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await Secp256k1.privateKeyToBytes({ privateKey: key }); - // Generate a digest of the data using the specified hash function. - const hashFunction = this.hashAlgorithms[hash]; - const digest = hashFunction(data); + // Generate a digest of the data using the SHA-256 hash function. + const digest = sha256(data); - // Signature operation returns a Signature instance with { r, s, recovery } properties. - const signatureObject = secp256k1.sign(digest, key); + // Sign the provided data using the ECDSA algorithm. + // The `secp256k1.sign` operation returns a signature object with { r, s, recovery } properties. + const signatureObject = secp256k1.sign(digest, privateKeyBytes); - // Convert Signature object to Uint8Array. + // Convert the signature object to Uint8Array. const signature = signatureObject.toCompactRawBytes(); return signature; } /** - * Validates a given private key to ensure that it's a valid 32-byte number - * that is less than the secp256k1 curve's order. + * Verifies an RFC6979-compliant ECDSA signature against given data and a secp256k1 public key. * - * This method checks the byte length of the key and its numerical validity - * according to the secp256k1 curve's parameters. It doesn't verify whether - * the key corresponds to a known or authorized entity or whether it has - * been compromised. + * This method validates a digital signature to ensure that it was generated by the holder of the + * corresponding private key and that the signed data has not been altered. The signature + * verification is performed using the ECDSA (Elliptic Curve Digital Signature Algorithm) as + * specified in RFC6979. The data to be verified is first hashed using the SHA-256 algorithm, and + * this hash is then used along with the public key to verify the signature. + * + * The method returns a boolean value indicating whether the signature is valid. A valid signature + * proves that the signed data was indeed signed by the owner of the private key corresponding to + * the provided public key and that the data has not been tampered with since it was signed. + * + * Note: The verification process does not consider the malleability of low-s signatures, which + * may be relevant in certain contexts, such as Bitcoin transactions. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); // Data that was signed + * const publicKey = { ... }; // Public key in JWK format corresponding to the private key that signed the data + * const signature = new Uint8Array([...]); // Signature to verify + * const isSignatureValid = await Secp256k1.verify({ + * data, + * key: publicKey, + * signature + * }); + * console.log(isSignatureValid); // true if the signature is valid, false otherwise + * ``` + * + * @param options - The options for the verification operation. + * @param options.data - The data that was signed, represented as a Uint8Array. + * @param options.key - The public key used for verification, represented in JWK format. + * @param options.signature - The signature to verify, represented as a Uint8Array. + * + * @returns A Promise that resolves to a boolean indicating whether the signature is valid. + */ + public static async verify(options: { + data: Uint8Array, + key: PublicKeyJwk, + signature: Uint8Array + }): Promise { + const { data, key, signature } = options; + + // Convert the public key from JWK format to bytes. + const publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey: key }); + + // Generate a digest of the data using the SHA-256 hash function. + const digest = sha256(data); + + /** Perform the verification of the signature. + * This verify operation has the malleability check disabled. Guaranteed support + * for low-s signatures across languages is unlikely especially in the context + * of SSI. Notable Cloud KMS providers do not natively support it either. It is + * also worth noting that low-s signatures are a requirement for Bitcoin. */ + const isValid = secp256k1.verify(signature, digest, publicKeyBytes, { lowS: false }); + + return isValid; + } + + /** + * Returns the elliptic curve points (x and y coordinates) for a given secp256k1 key. + * + * This method extracts the elliptic curve points from a given secp256k1 key, whether + * it's a private or a public key. For a private key, the method first computes the + * corresponding public key and then extracts the x and y coordinates. For a public key, + * it directly returns these coordinates. The coordinates are represented as Uint8Array. + * + * The x and y coordinates represent the key's position on the elliptic curve and can be + * used in various cryptographic operations, such as digital signatures or key agreement + * protocols. + * + * Example usage: + * + * ```ts + * // For a private key + * const privateKey = new Uint8Array([...]); // A 32-byte private key + * const { x: xFromPrivateKey, y: yFromPrivateKey } = await Secp256k1.getCurvePoints({ key: privateKey }); + * + * // For a public key + * const publicKey = new Uint8Array([...]); // A 33-byte or 65-byte public key + * const { x: xFromPublicKey, y: yFromPublicKey } = await Secp256k1.getCurvePoints({ key: publicKey }); + * ``` + * + * @param options - The options for the operation. + * @param options.key - The key for which to get the elliptic curve points. + * Can be either a private key or a public key. + * The key should be passed as a Uint8Array. + * + * @returns A Promise that resolves to an object with properties 'x' and 'y', + * each being a Uint8Array representing the x and y coordinates of the key point on the + * elliptic curve. + */ + private static async getCurvePoints(options: { + key: Uint8Array + }): Promise<{ x: Uint8Array, y: Uint8Array }> { + let { key } = options; + + // If key is a private key, first compute the public key. + if (key.byteLength === 32) { + key = secp256k1.getPublicKey(key); + } + + // Decode Weierstrass points from key bytes. + const point = secp256k1.ProjectivePoint.fromHex(key); + + // Get x- and y-coordinate values and convert to Uint8Array. + const x = numberToBytesBE(point.x, 32); + const y = numberToBytesBE(point.y, 32); + + return { x, y }; + } + + /** + * Validates a given private key to ensure its compliance with the secp256k1 curve standards. + * + * This method checks whether a provided private key is a valid 32-byte number and falls within + * the range defined by the secp256k1 curve's order. It is essential for ensuring the private + * key's mathematical correctness in the context of secp256k1-based cryptographic operations. + * + * Note that this validation strictly pertains to the key's format and numerical validity; it does + * not assess whether the key corresponds to a known entity or its security status (e.g., whether + * it has been compromised). + * + * Example usage: + * + * ```ts + * const privateKey = new Uint8Array([...]); // A 32-byte private key + * const isValid = await Secp256k1.validatePrivateKey({ key: privateKey }); + * console.log(isValid); // true or false based on the key's validity + * ``` * * @param options - The options for the key validation. * @param options.key - The private key to validate, represented as a Uint8Array. - * @returns A Promise that resolves to a boolean indicating whether the private - * key is a valid 32-byte number less than the secp256k1 curve's order. + * + * @returns A Promise that resolves to a boolean indicating whether the private key is valid. */ - public static async validatePrivateKey(options: { + private static async validatePrivateKey(options: { key: Uint8Array }): Promise { const { key } = options; @@ -253,24 +685,32 @@ export class Secp256k1 { } /** - * Validates a given public key to ensure that it corresponds to a - * valid point on the secp256k1 elliptic curve. + * Validates a given public key to confirm its mathematical correctness on the secp256k1 curve. * - * This method decodes the Weierstrass points from the key bytes and - * asserts their validity on the curve. If the points are not valid, - * the method returns false. If the points are valid, the method - * returns true. + * This method checks if the provided public key represents a valid point on the secp256k1 curve. + * It decodes the key's Weierstrass points (x and y coordinates) and verifies their validity + * against the curve's parameters. A valid point must lie on the curve and meet specific + * mathematical criteria defined by the curve's equation. * - * Note: This method does not check whether the key corresponds to a - * known or authorized entity, or whether it has been compromised. - * It only checks the mathematical validity of the key. + * It's important to note that this method does not verify the key's ownership or whether it has + * been compromised; it solely focuses on the key's adherence to the curve's mathematical + * principles. * - * @param options - The options for the key validation. - * @param options.key - The key to validate, represented as a Uint8Array. - * @returns A Promise that resolves to a boolean indicating whether the key - * corresponds to a valid point on the secp256k1 elliptic curve. + * Example usage: + * + * ```ts + * const publicKey = new Uint8Array([...]); // A public key in byte format + * const isValid = await Secp256k1.validatePublicKey({ key: publicKey }); + * console.log(isValid); // true if the key is valid on the secp256k1 curve, false otherwise + * ``` + * + * @param options - The options for the public key validation. + * @param options.key - The public key to validate, represented as a Uint8Array. + * + * @returns A Promise that resolves to a boolean indicating the public key's validity on + * the secp256k1 curve. */ - public static async validatePublicKey(options: { + private static async validatePublicKey(options: { key: Uint8Array }): Promise { const { key } = options; @@ -288,35 +728,4 @@ export class Secp256k1 { return true; } - - /** - * Verifies a RFC6979 ECDSA signature of given data with a given public key and hash algorithm. - * - * @param options - The options for the verification operation. - * @param options.data - The data that was signed. - * @param options.hash - The hash algorithm to use to generate a digest of the data. - * @param options.key - The public key to use for verification. - * @param options.signature - The signature to verify. - * @returns A Promise that resolves to a boolean indicating whether the signature is valid. - */ - public static async verify(options: { - data: Uint8Array, - hash: string, - key: Uint8Array, - signature: Uint8Array - }): Promise { - const { data, hash, key, signature } = options; - - // Generate a digest of the data using the specified hash function. - const hashFunction = this.hashAlgorithms[hash]; - const digest = hashFunction(data); - - // Verify operation with malleability check disabled. Guaranteed support for low-s - // signatures across languages is unlikely especially in the context of SSI. - // Notable Cloud KMS providers do not natively support it either. - // low-s signatures are a requirement for Bitcoin - const isValid = secp256k1.verify(signature, digest, key, { lowS: false }); - - return isValid; - } -} +} \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/x25519.ts b/packages/crypto/src/crypto-primitives/x25519.ts index 522b36bcd..63f20c32d 100644 --- a/packages/crypto/src/crypto-primitives/x25519.ts +++ b/packages/crypto/src/crypto-primitives/x25519.ts @@ -1,81 +1,369 @@ -import type { BytesKeyPair } from '../types/crypto-key.js'; - +import { Convert } from '@web5/common'; import { x25519 } from '@noble/curves/ed25519'; +import type { PrivateKeyJwk, PublicKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; + /** - * The `X25519` class provides an interface for X25519 (Curve25519) key pair - * generation, public key computation, and shared secret computation. The class - * uses the '@noble/curves/ed25519' package for the cryptographic operations. + * The `X25519` class provides a comprehensive suite of utilities for working with the X25519 + * elliptic curve, widely used for key agreement protocols and cryptographic applications. It + * provides methods for key generation, conversion, and Elliptic Curve Diffie-Hellman (ECDH) + * key agreement, all aligned with standard cryptographic practices. * - * All methods of this class are asynchronous and return Promises. They all use - * the Uint8Array type for keys and data, providing a consistent - * interface for working with binary data. + * The class supports conversions between raw byte formats and JSON Web Key (JWK) formats, + * making it versatile for various cryptographic tasks. It adheres to RFC6090 for ECDH, ensuring + * secure and effective handling of keys and cryptographic operations. * - * Example usage: + * Key Features: + * - Key Generation: Generate X25519 private keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Public Key Derivation: Derive public keys from private keys. + * - ECDH Shared Secret Computation: Securely derive shared secrets using private and public keys. + * + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments. + * + * Usage Examples: * * ```ts - * const ownKeyPair = await X25519.generateKeyPair(); - * const otherPartyKeyPair = await X25519.generateKeyPair(); + * // Key Generation + * const privateKey = await X25519.generateKey(); + * + * // Public Key Derivation + * const publicKey = await X25519.computePublicKey({ privateKey }); + * + * // ECDH Shared Secret Computation * const sharedSecret = await X25519.sharedSecret({ - * privateKey : ownKeyPair.privateKey, - * publicKey : otherPartyKeyPair.publicKey + * privateKeyA: privateKey, + * publicKeyB: anotherPublicKey * }); + * + * // Key Conversion + * const publicKeyBytes = await X25519.publicKeyToBytes({ publicKey }); + * const privateKeyBytes = await X25519.privateKeyToBytes({ privateKey }); * ``` */ export class X25519 { /** - * Generates a key pair for X25519 (private and public key). + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method accepts a private key as a byte array (Uint8Array) for the X25519 elliptic curve + * and transforms it into a JWK object. The process involves first deriving the public key from + * the private key, then encoding both the private and public keys into base64url format. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'OKP' for Octet Key Pair. + * - `crv`: Curve Name, set to 'X25519'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The derived public key, base64url-encoded. + * + * This method is useful for converting raw public keys into a standardized + * JSON format, facilitating their use in cryptographic operations and making + * them easy to share and store. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual private key bytes + * const privateKey = await X25519.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the private key conversion. + * @param options.privateKeyBytes - The raw private key as a Uint8Array. + * + * @returns A Promise that resolves to the private key in JWK format. + */ + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Derive the public key from the private key. + const publicKeyBytes = x25519.getPublicKey(privateKeyBytes); + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + kty : 'OKP', + crv : 'X25519', + d : Convert.uint8Array(privateKeyBytes).toBase64Url(), + x : Convert.uint8Array(publicKeyBytes).toBase64Url(), + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a raw public key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method accepts a public key as a byte array (Uint8Array) for the X25519 elliptic curve + * and transforms it into a JWK object. The conversion process involves encoding the public + * key bytes into base64url format. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'OKP' for Octet Key Pair. + * - `crv`: Curve Name, set to 'X25519'. + * - `x`: The public key, base64url-encoded. + * + * This method is useful for converting raw public keys into a standardized + * JSON format, facilitating their use in cryptographic operations and making + * them easy to share and store. + * + * Example usage: + * + * ```ts + * const publicKeyBytes = new Uint8Array([...]); // Replace with actual public key bytes + * const publicKey = await X25519.bytesToPublicKey({ publicKeyBytes }); + * ``` * - * @returns A Promise that resolves to a BytesKeyPair object. + * @param options - The options for the public key conversion. + * @param options.publicKeyBytes - The raw public key as a Uint8Array. + * + * @returns A Promise that resolves to the public key in JWK format. */ - public static async generateKeyPair(): Promise { - // Generate the private key and compute its public key. - const privateKey = x25519.utils.randomPrivateKey(); - const publicKey = x25519.getPublicKey(privateKey); - - const keyPair = { - privateKey : privateKey, - publicKey : publicKey + public static async bytesToPublicKey(options: { + publicKeyBytes: Uint8Array + }): Promise { + const { publicKeyBytes } = options; + + // Construct the public key in JWK format. + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'X25519', + x : Convert.uint8Array(publicKeyBytes).toBase64Url(), }; - return keyPair; + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await Jose.jwkThumbprint({ key: publicKey }); + + return publicKey; } /** - * Computes a public key given a private key. + * Derives the public key in JWK format from a given X25519 private key. + * + * This method takes a private key in JWK format and derives its corresponding public key, + * also in JWK format. The derivation process involves converting the private key to a + * raw byte array and then computing the corresponding public key on the Curve25519 curve. + * The public key is then encoded into base64url format to construct a JWK representation. + * + * The process ensures that the derived public key correctly corresponds to the given private key, + * adhering to the Curve25519 elliptic curve in Twisted Edwards form standards. This method is + * useful in cryptographic operations where a public key is needed for operations like signature + * verification, but only the private key is available. * - * @param options - The options for the public key computation operation. - * @param options.privateKey - The private key used to compute the public key. - * @returns A Promise that resolves to the computed public key as a Uint8Array. + * Example usage: + * + * ```ts + * const privateKey = { ... }; // A PrivateKeyJwk object representing an X25519 private key + * const publicKey = await X25519.computePublicKey({ privateKey }); + * ``` + * + * @param options - The options for the public key derivation. + * @param options.privateKey - The private key in JWK format from which to derive the public key. + * + * @returns A Promise that resolves to the derived public key in JWK format. */ - public static async getPublicKey(options: { - privateKey: Uint8Array - }): Promise { + public static async computePublicKey(options: { + privateKey: PrivateKeyJwk + }): Promise { let { privateKey } = options; - // Compute public key. - const publicKey = x25519.getPublicKey(privateKey); + // Convert the provided private key to a byte array. + const privateKeyBytes = await X25519.privateKeyToBytes({ privateKey }); + + // Derive the public key from the private key. + const publicKeyBytes = x25519.getPublicKey(privateKeyBytes); + + // Construct the public key in JWK format. + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'X25519', + x : Convert.uint8Array(publicKeyBytes).toBase64Url() + }; + + // Compute the JWK thumbprint and set as the key ID. + publicKey.kid = await Jose.jwkThumbprint({ key: publicKey }); return publicKey; } /** - * Generates a RFC6090 ECDH shared secret given the private key of one party - * and the public key of another party. + * Generates an X25519 private key in JSON Web Key (JWK) format. + * + * This method creates a new private key suitable for use with the X25519 elliptic curve. + * The key generation process involves using cryptographically secure random number generation + * to ensure the uniqueness and security of the key. The resulting private key adheres to the + * JWK format making it compatible with common cryptographic standards and easy to use in various + * cryptographic processes. + * + * The generated private key in JWK format includes the following components: + * - `kty`: Key Type, set to 'OKP' for Octet Key Pair. + * - `crv`: Curve Name, set to 'X25519'. + * - `d`: The private key component, base64url-encoded. + * - `x`: The derived public key, base64url-encoded. + * + * The key is returned in a format suitable for direct use in key agreement operations. + * + * Example usage: + * + * ```ts + * const privateKey = await X25519.generateKey(); + * ``` + * + * @returns A Promise that resolves to the generated private key in JWK format. + */ + public static async generateKey(): Promise { + // Generate a random private key. + const privateKeyBytes = x25519.utils.randomPrivateKey(); + + // Convert private key from bytes to JWK format. + const privateKey = await X25519.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method accepts a private key in JWK format and extracts its raw byte representation. + * + * This method accepts a public key in JWK format and converts it into its raw binary + * form. The conversion process involves decoding the 'd' parameter of the JWK + * from base64url format into a byte array. + * + * This conversion is essential for operations that require the private key in its raw + * binary form, such as certain low-level cryptographic operations or when interfacing + * with systems and libraries that expect keys in a byte array format. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // An X25519 private key in JWK format + * const privateKeyBytes = await X25519.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the private key conversion. + * @param options.privateKey - The private key in JWK format. + * + * @returns A Promise that resolves to the private key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Verify the provided JWK represents a valid OKP private key. + if (!Jose.isOkpPrivateKeyJwk(privateKey)) { + throw new Error(`X25519: The provided key is not a valid OKP private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + + return privateKeyBytes; + } + + /** + * Converts a public key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method accepts a public key in JWK format and converts it into its raw binary form. + * The conversion process involves decoding the 'x' parameter of the JWK (which represent the + * x coordinate of the elliptic curve point) from base64url format into a byte array. + * + * This conversion is essential for operations that require the public key in its raw + * binary form, such as certain low-level cryptographic operations or when interfacing + * with systems and libraries that expect keys in a byte array format. + * + * Example usage: + * + * ```ts + * const publicKey = { ... }; // An X25519 public key in JWK format + * const publicKeyBytes = await X25519.publicKeyToBytes({ publicKey }); + * ``` + * + * @param options - The options for the public key conversion. + * @param options.publicKey - The public key in JWK format. + * + * @returns A Promise that resolves to the public key as a Uint8Array. + */ + public static async publicKeyToBytes(options: { + publicKey: PublicKeyJwk + }): Promise { + const { publicKey } = options; + + // Verify the provided JWK represents a valid OKP public key. + if (!Jose.isOkpPublicKeyJwk(publicKey)) { + throw new Error(`X25519: The provided key is not a valid OKP public key.`); + } + + // Decode the provided public key to bytes. + const publicKeyBytes = Convert.base64Url(publicKey.x).toUint8Array(); + + return publicKeyBytes; + } + + /** + * Computes an RFC6090-compliant Elliptic Curve Diffie-Hellman (ECDH) shared secret + * using secp256k1 private and public keys in JSON Web Key (JWK) format. + * + * This method facilitates the ECDH key agreement protocol, which is a method of securely + * deriving a shared secret between two parties based on their private and public keys. + * It takes the private key of one party (privateKeyA) and the public key of another + * party (publicKeyB) to compute a shared secret. The shared secret is derived from the + * x-coordinate of the elliptic curve point resulting from the multiplication of the + * public key with the private key. + * + * Note: When performing Elliptic Curve Diffie-Hellman (ECDH) key agreement, + * the resulting shared secret is a point on the elliptic curve, which + * consists of an x-coordinate and a y-coordinate. With a 256-bit curve like + * secp256k1, each of these coordinates is 32 bytes (256 bits) long. However, + * in the ECDH process, it's standard practice to use only the x-coordinate + * of the shared secret point as the resulting shared key. This is because + * the y-coordinate does not add to the entropy of the key, and both parties + * can independently compute the x-coordinate. Consquently, this implementation + * omits the y-coordinate for simplicity and standard compliance. + * + * Example usage: + * + * ```ts + * const privateKeyA = { ... }; // A PrivateKeyJwk object for party A + * const publicKeyB = { ... }; // A PublicKeyJwk object for party B + * const sharedSecret = await Secp256k1.sharedSecret({ + * privateKeyA, + * publicKeyB + * }); + * ``` * * @param options - The options for the shared secret computation operation. - * @param options.privateKey - The private key of one party. - * @param options.publicKey - The public key of the other party. + * @param options.privateKeyA - The private key in JWK format of one party. + * @param options.publicKeyB - The public key in JWK format of the other party. + * * @returns A Promise that resolves to the computed shared secret as a Uint8Array. */ public static async sharedSecret(options: { - privateKey: Uint8Array, - publicKey: Uint8Array + privateKeyA: PrivateKeyJwk, + publicKeyB: PublicKeyJwk }): Promise { - let { privateKey, publicKey } = options; + let { privateKeyA, publicKeyB } = options; + // Ensure that keys from the same key pair are not specified. + if ('x' in privateKeyA && 'x' in publicKeyB && privateKeyA.x === publicKeyB.x) { + throw new Error(`X25519: ECDH shared secret cannot be computed from a single key pair's public and private keys.`); + } - const sharedSecret = x25519.getSharedSecret(privateKey, publicKey); + // Convert the provided private and public keys to bytes. + const privateKeyABytes = await X25519.privateKeyToBytes({ privateKey: privateKeyA }); + const publicKeyBBytes = await X25519.publicKeyToBytes({ publicKey: publicKeyB }); + + // Compute the shared secret between the public and private keys. + const sharedSecret = x25519.getSharedSecret(privateKeyABytes, publicKeyBBytes); return sharedSecret; } @@ -89,13 +377,14 @@ export class X25519 { * @param options - The options for the key validation operation. * @param options.key - The key to validate. * @throws {Error} If the method is called because it is not yet implemented. + * * @returns A Promise that resolves to void. */ - public static async validatePublicKey(_options: { + private static async validatePublicKey(_options: { key: Uint8Array }): Promise { // TODO: Implement once/if @noble/curves library implements checking // proper points on the Montgomery curve. - throw new Error(`Not implemented: 'validatePublicKey()'`); + throw new Error(`X25519: Not implemented: 'validatePublicKey()'`); } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/xchacha20-poly1305.ts b/packages/crypto/src/crypto-primitives/xchacha20-poly1305.ts index 5aeb4c012..264b36478 100644 --- a/packages/crypto/src/crypto-primitives/xchacha20-poly1305.ts +++ b/packages/crypto/src/crypto-primitives/xchacha20-poly1305.ts @@ -1,34 +1,202 @@ -import { xchacha20_poly1305 } from '@noble/ciphers/chacha'; +import { Convert } from '@web5/common'; +import { xchacha20poly1305 } from '@noble/ciphers/chacha'; + +import type { PrivateKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; const TAG_LENGTH = 16; +/** + * The `XChaCha20Poly1305` class provides a suite of utilities for cryptographic operations + * using the XChaCha20-Poly1305 algorithm, a combination of the XChaCha20 stream cipher and the + * Poly1305 message authentication code (MAC). This class encompasses methods for key generation, + * encryption, decryption, and conversions between raw byte arrays and JSON Web Key (JWK) formats. + * + * XChaCha20-Poly1305 is renowned for its high security and efficiency, especially in scenarios + * involving large data volumes or where data integrity and confidentiality are paramount. The + * extended nonce size of XChaCha20 reduces the risks of nonce reuse, while Poly1305 provides + * a strong MAC ensuring data integrity. + * + * Key Features: + * - Key Generation: Generate XChaCha20-Poly1305 symmetric keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Encryption: Encrypt data using XChaCha20-Poly1305, returning both ciphertext and MAC tag. + * - Decryption: Decrypt data and verify integrity using the XChaCha20-Poly1305 algorithm. + * + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments. + * + * Usage Examples: + * + * ```ts + * // Key Generation + * const privateKey = await XChaCha20Poly1305.generateKey(); + * + * // Encryption + * const data = new TextEncoder().encode('Hello, world!'); + * const nonce = crypto.getRandomValues(new Uint8Array(24)); // 24-byte nonce + * const additionalData = new TextEncoder().encode('Associated data'); + * const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + * data, + * nonce, + * additionalData, + * key: privateKey + * }); + * + * // Decryption + * const decryptedData = await XChaCha20Poly1305.decrypt({ + * data: ciphertext, + * nonce, + * tag, + * additionalData, + * key: privateKey + * }); + * + * // Key Conversion + * const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey }); + * ``` + */ export class XChaCha20Poly1305 { + /** + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method takes a symmetric key represented as a byte array (Uint8Array) and converts it into + * a JWK object for use with the XChaCha20-Poly1305 algorithm. The process involves encoding the + * key into base64url format and setting the appropriate JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'oct' for Octet Sequence (representing a symmetric key). + * - `k`: The symmetric key, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual symmetric key bytes + * const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKeyBytes - The raw symmetric key as a Uint8Array. + * + * @returns A Promise that resolves to the symmetric key in JWK format. + */ + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + k : Convert.uint8Array(privateKeyBytes).toBase64Url(), + kty : 'oct' + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + return privateKey; + } + + /** + * Decrypts the provided data using XChaCha20-Poly1305. + * + * This method performs XChaCha20-Poly1305 decryption on the given encrypted data using the + * specified key, nonce, and authentication tag. It supports optional additional authenticated + * data (AAD) for enhanced security. The nonce must be 24 bytes long, consistent with XChaCha20's + * specifications. + * + * Example usage: + * + * ```ts + * const encryptedData = new Uint8Array([...]); // Encrypted data + * const nonce = new Uint8Array(24); // 24-byte nonce + * const tag = new Uint8Array([...]); // Authentication tag + * const additionalData = new Uint8Array([...]); // Optional AAD + * const key = { ... }; // A PrivateKeyJwk object representing the XChaCha20-Poly1305 key + * const decryptedData = await XChaCha20Poly1305.decrypt({ + * data: encryptedData, + * nonce, + * tag, + * additionalData, + * key + * }); + * ``` + * + * @param options - The options for the decryption operation. + * @param options.data - The encrypted data to decrypt, represented as a Uint8Array. + * @param options.key - The key to use for decryption, represented in JWK format. + * @param options.nonce - The nonce used during the encryption process. + * @param options.tag - The authentication tag generated during encryption. + * @param options.additionalData - Optional additional authenticated data. + * + * @returns A Promise that resolves to the decrypted data as a Uint8Array. + */ public static async decrypt(options: { additionalData?: Uint8Array, data: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, nonce: Uint8Array, tag: Uint8Array }): Promise { const { additionalData, data, key, nonce, tag } = options; - const xc20p = xchacha20_poly1305(key, nonce, additionalData); + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey: key }); + + const xc20p = xchacha20poly1305(privateKeyBytes, nonce, additionalData); const ciphertext = new Uint8Array([...data, ...tag]); const plaintext = xc20p.decrypt(ciphertext); return plaintext; } + /** + * Encrypts the provided data using XChaCha20-Poly1305. + * + * This method performs XChaCha20-Poly1305 encryption on the given data using the specified key + * and nonce. It supports optional additional authenticated data (AAD) for enhanced security. The + * nonce must be 24 bytes long, as per XChaCha20's specifications. The method returns the + * encrypted data along with an authentication tag as a Uint8Array, ensuring both confidentiality + * and integrity of the data. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); + * const nonce = crypto.getRandomValues(new Uint8Array(24)); // 24-byte nonce + * const additionalData = new TextEncoder().encode('Associated data'); // Optional AAD + * const key = { ... }; // A PrivateKeyJwk object representing an XChaCha20-Poly1305 key + * const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + * data, + * nonce, + * additionalData, + * key + * }); + * ``` + * + * @param options - The options for the encryption operation. + * @param options.data - The data to encrypt, represented as a Uint8Array. + * @param options.key - The key to use for encryption, represented in JWK format. + * @param options.nonce - A 24-byte nonce for the encryption process. + * @param options.additionalData - Optional additional authenticated data. + * + * @returns A Promise that resolves to an object containing the encrypted data (`ciphertext`) and + * the authentication tag (`tag`). + */ public static async encrypt(options: { additionalData?: Uint8Array, data: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, nonce: Uint8Array }): Promise<{ ciphertext: Uint8Array, tag: Uint8Array }> { const { additionalData, data, key, nonce } = options; - const xc20p = xchacha20_poly1305(key, nonce, additionalData); + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey: key }); + + const xc20p = xchacha20poly1305(privateKeyBytes, nonce, additionalData); const cipherOutput = xc20p.encrypt(data); const ciphertext = cipherOutput.subarray(0, -TAG_LENGTH); @@ -37,10 +205,72 @@ export class XChaCha20Poly1305 { return { ciphertext, tag }; } - public static async generateKey(): Promise { - // Generate the secret key. - const secretKey = crypto.getRandomValues(new Uint8Array(32)); + /** + * Generates a symmetric key for XChaCha20-Poly1305 in JSON Web Key (JWK) format. + * + * This method creates a new symmetric key suitable for use with the XChaCha20-Poly1305 algorithm. + * The key is generated using cryptographically secure random number generation to ensure its + * uniqueness and security. The XChaCha20-Poly1305 algorithm requires a 256-bit key (32 bytes), + * and this method adheres to that specification. + * + * Key components included in the JWK: + * - `kty`: Key Type, set to 'oct' for Octet Sequence. + * - `k`: The symmetric key component, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: + * + * ```ts + * const privateKey = await XChaCha20Poly1305.generateKey(); + * ``` + * + * @returns A Promise that resolves to the generated symmetric key in JWK format. + */ + public static async generateKey(): Promise { + // Generate a random private key. + const privateKeyBytes = crypto.getRandomValues(new Uint8Array(32)); + + // Convert private key from bytes to JWK format. + const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method takes a symmetric key in JWK format and extracts its raw byte representation. + * It decodes the 'k' parameter of the JWK value, which represents the symmetric key in base64url + * encoding, into a byte array. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // A symmetric key in JWK format + * const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKey - The symmetric key in JWK format. + * + * @returns A Promise that resolves to the symmetric key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Verify the provided JWK represents a valid oct private key. + if (!Jose.isOctPrivateKeyJwk(privateKey)) { + throw new Error(`XChaCha20Poly1305: The provided key is not a valid oct private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); - return secretKey; + return privateKeyBytes; } } \ No newline at end of file diff --git a/packages/crypto/src/crypto-primitives/xchacha20.ts b/packages/crypto/src/crypto-primitives/xchacha20.ts index f1a9e916f..9d741d9c0 100644 --- a/packages/crypto/src/crypto-primitives/xchacha20.ts +++ b/packages/crypto/src/crypto-primitives/xchacha20.ts @@ -1,34 +1,250 @@ +import { Convert } from '@web5/common'; import { xchacha20 } from '@noble/ciphers/chacha'; +import type { PrivateKeyJwk } from '../jose.js'; + +import { Jose } from '../jose.js'; + +/** + * The `XChaCha20` class provides a comprehensive suite of utilities for cryptographic operations + * using the XChaCha20 symmetric encryption algorithm. This class includes methods for key + * generation, encryption, decryption, and conversions between raw byte arrays and JSON Web Key + * (JWK) formats. XChaCha20 is an extended nonce variant of ChaCha20, a stream cipher designed for + * high-speed encryption with substantial security margins. + * + * The XChaCha20 algorithm is particularly well-suited for encrypting large volumes of data or + * data streams, especially where random access is required. The class adheres to standard + * cryptographic practices, ensuring robustness and security in its implementations. + * + * Key Features: + * - Key Generation: Generate XChaCha20 symmetric keys in JWK format. + * - Key Conversion: Transform keys between raw byte arrays and JWK formats. + * - Encryption: Encrypt data using XChaCha20 with the provided symmetric key. + * - Decryption: Decrypt data encrypted with XChaCha20 using the corresponding symmetric key. + * + * The methods in this class are asynchronous, returning Promises to accommodate various + * JavaScript environments. + * + * Usage Examples: + * + * ```ts + * // Key Generation + * const privateKey = await XChaCha20.generateKey(); + * + * // Encryption + * const data = new TextEncoder().encode('Hello, world!'); + * const nonce = crypto.getRandomValues(new Uint8Array(24)); // 24-byte nonce for XChaCha20 + * const encryptedData = await XChaCha20.encrypt({ + * data, + * nonce, + * key: privateKey + * }); + * + * // Decryption + * const decryptedData = await XChaCha20.decrypt({ + * data: encryptedData, + * nonce, + * key: privateKey + * }); + * + * // Key Conversion + * const privateKeyBytes = await XChaCha20.privateKeyToBytes({ privateKey }); + * ``` + */ export class XChaCha20 { + /** + * Converts a raw private key in bytes to its corresponding JSON Web Key (JWK) format. + * + * This method takes a symmetric key represented as a byte array (Uint8Array) and + * converts it into a JWK object for use with the XChaCha20 symmetric encryption algorithm. The + * conversion process involves encoding the key into base64url format and setting the appropriate + * JWK parameters. + * + * The resulting JWK object includes the following properties: + * - `kty`: Key Type, set to 'oct' for Octet Sequence (representing a symmetric key). + * - `k`: The symmetric key, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: + * + * ```ts + * const privateKeyBytes = new Uint8Array([...]); // Replace with actual symmetric key bytes + * const privateKey = await XChaCha20.bytesToPrivateKey({ privateKeyBytes }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKeyBytes - The raw symmetric key as a Uint8Array. + * + * @returns A Promise that resolves to the symmetric key in JWK format. + */ + public static async bytesToPrivateKey(options: { + privateKeyBytes: Uint8Array + }): Promise { + const { privateKeyBytes } = options; + + // Construct the private key in JWK format. + const privateKey: PrivateKeyJwk = { + k : Convert.uint8Array(privateKeyBytes).toBase64Url(), + kty : 'oct' + }; + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Decrypts the provided data using XChaCha20. + * + * This method performs XChaCha20 decryption on the given encrypted data using the specified key + * and nonce. The nonce should be the same as used in the encryption process and must be 24 bytes + * long. The method returns the decrypted data as a Uint8Array. + * + * Example usage: + * + * ```ts + * const encryptedData = new Uint8Array([...]); // Encrypted data + * const nonce = new Uint8Array(24); // 24-byte nonce used during encryption + * const key = { ... }; // A PrivateKeyJwk object representing the XChaCha20 key + * const decryptedData = await XChaCha20.decrypt({ + * data: encryptedData, + * nonce, + * key + * }); + * ``` + * + * @param options - The options for the decryption operation. + * @param options.data - The encrypted data to decrypt, represented as a Uint8Array. + * @param options.key - The key to use for decryption, represented in JWK format. + * @param options.nonce - The nonce used during the encryption process. + * + * @returns A Promise that resolves to the decrypted data as a Uint8Array. + */ public static async decrypt(options: { data: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, nonce: Uint8Array }): Promise { const { data, key, nonce } = options; - const ciphertext = xchacha20(key, nonce, data); + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await XChaCha20.privateKeyToBytes({ privateKey: key }); + + const ciphertext = xchacha20(privateKeyBytes, nonce, data); return ciphertext; } + /** + * Encrypts the provided data using XChaCha20. + * + * This method performs XChaCha20 encryption on the given data using the specified key and nonce. + * The nonce must be 24 bytes long, ensuring a high level of security through a vast nonce space, + * reducing the risks associated with nonce reuse. The method returns the encrypted data as a + * Uint8Array. + * + * Example usage: + * + * ```ts + * const data = new TextEncoder().encode('Hello, world!'); + * const nonce = crypto.getRandomValues(new Uint8Array(24)); // 24-byte nonce for XChaCha20 + * const key = { ... }; // A PrivateKeyJwk object representing an XChaCha20 key + * const encryptedData = await XChaCha20.encrypt({ + * data, + * nonce, + * key + * }); + * ``` + * + * @param options - The options for the encryption operation. + * @param options.data - The data to encrypt, represented as a Uint8Array. + * @param options.key - The key to use for encryption, represented in JWK format. + * @param options.nonce - A 24-byte nonce for the encryption process. + * + * @returns A Promise that resolves to the encrypted data as a Uint8Array. + */ public static async encrypt(options: { data: Uint8Array, - key: Uint8Array, + key: PrivateKeyJwk, nonce: Uint8Array }): Promise { const { data, key, nonce } = options; - const plaintext = xchacha20(key, nonce, data); + // Convert the private key from JWK format to bytes. + const privateKeyBytes = await XChaCha20.privateKeyToBytes({ privateKey: key }); + + const plaintext = xchacha20(privateKeyBytes, nonce, data); return plaintext; } - public static async generateKey(): Promise { - // Generate the secret key. - const secretKey = crypto.getRandomValues(new Uint8Array(32)); + /** + * Generates a symmetric key for XChaCha20 in JSON Web Key (JWK) format. + * + * This method creates a new symmetric key suitable for use with the XChaCha20 encryption + * algorithm. The key is generated using cryptographically secure random number generation + * to ensure its uniqueness and security. The XChaCha20 algorithm requires a 256-bit key + * (32 bytes), and this method adheres to that specification. + * + * Key components included in the JWK: + * - `kty`: Key Type, set to 'oct' for Octet Sequence. + * - `k`: The symmetric key component, base64url-encoded. + * - `kid`: Key ID, generated based on the JWK thumbprint. + * + * Example usage: + * + * ```ts + * const privateKey = await XChaCha20.generateKey(); + * ``` + * + * @returns A Promise that resolves to the generated symmetric key in JWK format. + */ + public static async generateKey(): Promise { + // Generate a random private key. + const privateKeyBytes = crypto.getRandomValues(new Uint8Array(32)); + + // Convert private key from bytes to JWK format. + const privateKey = await XChaCha20.bytesToPrivateKey({ privateKeyBytes }); + + // Compute the JWK thumbprint and set as the key ID. + privateKey.kid = await Jose.jwkThumbprint({ key: privateKey }); + + return privateKey; + } + + /** + * Converts a private key from JSON Web Key (JWK) format to a raw byte array (Uint8Array). + * + * This method takes a symmetric key in JWK format and extracts its raw byte representation. + * It decodes the 'k' parameter of the JWK value, which represents the symmetric key in base64url + * encoding, into a byte array. + * + * Example usage: + * + * ```ts + * const privateKey = { ... }; // A symmetric key in JWK format + * const privateKeyBytes = await XChaCha20.privateKeyToBytes({ privateKey }); + * ``` + * + * @param options - The options for the symmetric key conversion. + * @param options.privateKey - The symmetric key in JWK format. + * + * @returns A Promise that resolves to the symmetric key as a Uint8Array. + */ + public static async privateKeyToBytes(options: { + privateKey: PrivateKeyJwk + }): Promise { + const { privateKey } = options; + + // Verify the provided JWK represents a valid oct private key. + if (!Jose.isOctPrivateKeyJwk(privateKey)) { + throw new Error(`XChaCha20: The provided key is not a valid oct private key.`); + } + + // Decode the provided private key to bytes. + const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); - return secretKey; + return privateKeyBytes; } } \ No newline at end of file diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 2ed684c02..44e4612c4 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -1,4 +1,3 @@ -export type * from './types/crypto-key.js'; export type * from './types/web5-crypto.js'; export * from './algorithms-api/index.js'; diff --git a/packages/crypto/src/jose.ts b/packages/crypto/src/jose.ts index 561bcc459..ff373b3e1 100644 --- a/packages/crypto/src/jose.ts +++ b/packages/crypto/src/jose.ts @@ -1,31 +1,68 @@ import { sha256 } from '@noble/hashes/sha256'; import { Convert, Multicodec, MulticodecCode, MulticodecDefinition, removeUndefinedProperties } from '@web5/common'; -import type { Web5Crypto } from './types/web5-crypto.js'; - import { keyToMultibaseId } from './utils.js'; -import { CryptoKey } from './algorithms-api/index.js'; import { Ed25519, Secp256k1, X25519 } from './crypto-primitives/index.js'; /** * JSON Web Key Operations * - * decrypt : Decrypt content and validate decryption, if applicable - * deriveBits : Derive bits not to be used as a key - * deriveKey : Derive key - * encrypt : Encrypt content - * sign : Compute digital signature or MAC - * unwrapKey : Decrypt key and validate decryption, if applicable - * verify : Verify digital signature or MAC - * wrapKey : Encrypt key + * The "key_ops" (key operations) parameter identifies the operation(s) + * for which the key is intended to be used. The "key_ops" parameter is + * intended for use cases in which public, private, or symmetric keys + * may be present. + * + * Its value is an array of key operation values. Values defined by + * {@link https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3 | RFC 7517 Section 4.3} are: + * + * - "decrypt" : Decrypt content and validate decryption, if applicable + * - "deriveBits" : Derive bits not to be used as a key + * - "deriveKey" : Derive key + * - "encrypt" : Encrypt content + * - "sign" : Compute digital signature or MAC + * - "unwrapKey" : Decrypt key and validate decryption, if applicable + * - "verify" : Verify digital signature or MAC + * - "wrapKey" : Encrypt key + * + * Other values MAY be used. The key operation values are case- + * sensitive strings. Duplicate key operation values MUST NOT be + * present in the array. Use of the "key_ops" member is OPTIONAL, + * unless the application requires its presence. + * + * The "use" and "key_ops" JWK members SHOULD NOT be used together; + * however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they + * use, if either is to be used by the application. */ -export type JwkOperation = Web5Crypto.KeyUsage[] | string[]; +export type JwkOperation = 'encrypt' | 'decrypt' | 'sign' | 'verify' | 'deriveKey' | 'deriveBits' | 'wrapKey' | 'unwrapKey'; /** * JSON Web Key Use * - * sig : Digital Signature or MAC - * enc : Encryption + * The "use" (public key use) parameter identifies the intended use of + * the public key. The "use" parameter is employed to indicate whether + * a public key is used for encrypting data or verifying the signature + * on data. + * + * Values defined by {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 | RFC 7517 Section 4.2} are: + * + * - "sig" (signature) + * - "enc" (encryption) + * + * Other values MAY be used. The "use" value is a case-sensitive + * string. Use of the "use" member is OPTIONAL, unless the application + * requires its presence. + * + * The "use" and "key_ops" JWK members SHOULD NOT be used together; + * however, if both are used, the information they convey MUST be + * consistent. Applications should specify which of these members they + * use, if either is to be used by the application. + * + * When a key is used to wrap another key and a public key use + * designation for the first key is desired, the "enc" (encryption) key + * use value is used, since key wrapping is a kind of encryption. The + * "enc" value is also to be used for public keys used for key agreement + * operations. */ export type JwkUse = 'sig' | 'enc' | string; @@ -90,8 +127,8 @@ export type JwkParamsAnyKeyType = { // Extractable ext?: 'true' | 'false'; // Key Operations - key_ops?: JwkOperation; - // Key ID + key_ops?: JwkOperation[]; + //'encrypt' | 'decrypt' | 'sign' | 'verify' | 'deriveKey' | 'deriveBits' | 'wrapKey' | 'unwrapKey';D kid?: string; // Key Type kty: JwkType; @@ -411,59 +448,13 @@ export interface JweHeaderParams extends JoseHeaderParams { [key: string]: unknown } -const joseToWebCryptoMapping: { [key: string]: Web5Crypto.GenerateKeyOptions } = { - 'Ed25519' : { name: 'EdDSA', namedCurve: 'Ed25519' }, - 'Ed448' : { name: 'EdDSA', namedCurve: 'Ed448' }, - 'X25519' : { name: 'ECDH', namedCurve: 'X25519' }, - 'secp256k1:ES256K' : { name: 'ECDSA', namedCurve: 'secp256k1' }, - 'secp256k1' : { name: 'ECDH', namedCurve: 'secp256k1' }, - 'P-256' : { name: 'ECDSA', namedCurve: 'P-256' }, - 'P-384' : { name: 'ECDSA', namedCurve: 'P-384' }, - 'P-521' : { name: 'ECDSA', namedCurve: 'P-521' }, - 'A128CBC' : { name: 'AES-CBC', length: 128 }, - 'A192CBC' : { name: 'AES-CBC', length: 192 }, - 'A256CBC' : { name: 'AES-CBC', length: 256 }, - 'A128CTR' : { name: 'AES-CTR', length: 128 }, - 'A192CTR' : { name: 'AES-CTR', length: 192 }, - 'A256CTR' : { name: 'AES-CTR', length: 256 }, - 'A128GCM' : { name: 'AES-GCM', length: 128 }, - 'A192GCM' : { name: 'AES-GCM', length: 192 }, - 'A256GCM' : { name: 'AES-GCM', length: 256 }, - 'HS256' : { name: 'HMAC', hash: { name: 'SHA-256' } }, - 'HS384' : { name: 'HMAC', hash: { name: 'SHA-384' } }, - 'HS512' : { name: 'HMAC', hash: { name: 'SHA-512' } }, -}; - -const webCryptoToJoseMapping: { [key: string]: Partial } = { - 'EdDSA:Ed25519' : { alg: 'EdDSA', crv: 'Ed25519', kty: 'OKP' }, - 'EdDSA:Ed448' : { alg: 'EdDSA', crv: 'Ed448', kty: 'OKP' }, - 'ECDH:X25519' : { crv: 'X25519', kty: 'OKP' }, - 'ECDSA:secp256k1' : { alg: 'ES256K', crv: 'secp256k1', kty: 'EC' }, - 'ECDH:secp256k1' : { crv: 'secp256k1', kty: 'EC' }, - 'ECDSA:P-256' : { alg: 'ES256', crv: 'P-256', kty: 'EC' }, - 'ECDSA:P-384' : { alg: 'ES384', crv: 'P-384', kty: 'EC' }, - 'ECDSA:P-521' : { alg: 'ES512', crv: 'P-521', kty: 'EC' }, - 'AES-CBC:128' : { alg: 'A128CBC', kty: 'oct' }, - 'AES-CBC:192' : { alg: 'A192CBC', kty: 'oct' }, - 'AES-CBC:256' : { alg: 'A256CBC', kty: 'oct' }, - 'AES-CTR:128' : { alg: 'A128CTR', kty: 'oct' }, - 'AES-CTR:192' : { alg: 'A192CTR', kty: 'oct' }, - 'AES-CTR:256' : { alg: 'A256CTR', kty: 'oct' }, - 'AES-GCM:128' : { alg: 'A128GCM', kty: 'oct' }, - 'AES-GCM:192' : { alg: 'A192GCM', kty: 'oct' }, - 'AES-GCM:256' : { alg: 'A256GCM', kty: 'oct' }, - 'HMAC:SHA-256' : { alg: 'HS256', kty: 'oct' }, - 'HMAC:SHA-384' : { alg: 'HS384', kty: 'oct' }, - 'HMAC:SHA-512' : { alg: 'HS512', kty: 'oct' }, -}; - const multicodecToJoseMapping: { [key: string]: JsonWebKey } = { - 'ed25519-pub' : { alg: 'EdDSA', crv: 'Ed25519', kty: 'OKP', x: '' }, - 'ed25519-priv' : { alg: 'EdDSA', crv: 'Ed25519', kty: 'OKP', x: '', d: '' }, - 'secp256k1-pub' : { alg: 'ES256K', crv: 'secp256k1', kty: 'EC', x: '', y: ''}, - 'secp256k1-priv' : { alg: 'ES256K', crv: 'secp256k1', kty: 'EC', x: '', y: '', d: '' }, - 'x25519-pub' : { crv: 'X25519', kty: 'OKP', x: '' }, - 'x25519-priv' : { crv: 'X25519', kty: 'OKP', x: '', d: '' }, + 'ed25519-pub' : { crv: 'Ed25519', kty: 'OKP', x: '' }, + 'ed25519-priv' : { crv: 'Ed25519', kty: 'OKP', x: '', d: '' }, + 'secp256k1-pub' : { crv: 'secp256k1', kty: 'EC', x: '', y: ''}, + 'secp256k1-priv' : { crv: 'secp256k1', kty: 'EC', x: '', y: '', d: '' }, + 'x25519-pub' : { crv: 'X25519', kty: 'OKP', x: '' }, + 'x25519-priv' : { crv: 'X25519', kty: 'OKP', x: '', d: '' }, }; const joseToMulticodecMapping: { [key: string]: string } = { @@ -476,43 +467,51 @@ const joseToMulticodecMapping: { [key: string]: string } = { }; export class Jose { + public static isEcPrivateKeyJwk(obj: unknown): obj is JwkParamsEcPrivate { + if (!obj || typeof obj !== 'object') return false; + if (!('kty' in obj && 'crv' in obj && 'x' in obj && 'd' in obj)) return false; + if (obj.kty !== 'EC') return false; + if (typeof obj.d !== 'string') return false; + if (typeof obj.x !== 'string') return false; + + return true; + } - public static async cryptoKeyToJwk(options: { - key: Web5Crypto.CryptoKey, - }): Promise { - const { algorithm, extractable, material, type, usages } = options.key; - - // Translate WebCrypto algorithm to JOSE format. - let jsonWebKey = Jose.webCryptoToJose(algorithm) as JsonWebKey; - - // Set extractable parameter. - jsonWebKey.ext = extractable ? 'true' : 'false'; - - // Set key use parameter. - jsonWebKey.key_ops = usages; + public static isEcPublicKeyJwk(obj: unknown): obj is JwkParamsEcPublic { + if (!obj || typeof obj !== 'object') return false; + if (!('kty' in obj && 'crv' in obj && 'x' in obj)) return false; + if ('d' in obj) return false; + if (obj.kty !== 'EC') return false; + if (typeof obj.x !== 'string') return false; + return true; + } - jsonWebKey = await Jose.keyToJwk({ - keyMaterial : material, - keyType : type, - ...jsonWebKey - }); + public static isOctPrivateKeyJwk(obj: unknown): obj is JwkParamsOctPrivate { + if (!obj || typeof obj !== 'object') return false; + if (!('kty' in obj && 'k' in obj)) return false; + if (obj.kty !== 'oct') return false; + if (typeof obj.k !== 'string') return false; - return { ...jsonWebKey }; + return true; } - public static async cryptoKeyToJwkPair(options: { - keyPair: Web5Crypto.CryptoKeyPair, - }): Promise { - const { keyPair } = options; + public static isOkpPrivateKeyJwk(obj: unknown): obj is JwkParamsOkpPrivate { + if (!obj || typeof obj !== 'object') return false; + if (!('kty' in obj && 'crv' in obj && 'x' in obj && 'd' in obj)) return false; + if (obj.kty !== 'OKP') return false; + if (typeof obj.d !== 'string') return false; + if (typeof obj.x !== 'string') return false; - // Convert public and private keys into JSON Web Key format. - const privateKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.privateKey }) as PrivateKeyJwk; - const publicKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.publicKey }) as PublicKeyJwk; - - // Assemble as a JWK key pair - const jwkKeyPair: JwkKeyPair = { privateKeyJwk, publicKeyJwk }; + return true; + } - return { ...jwkKeyPair }; + public static isOkpPublicKeyJwk(obj: unknown): obj is JwkParamsOkpPublic { + if (!obj || typeof obj !== 'object') return false; + if ('d' in obj) return false; + if (!('kty' in obj && 'crv' in obj && 'x' in obj)) return false; + if (obj.kty !== 'OKP') return false; + if (typeof obj.x !== 'string') return false; + return true; } public static async joseToMulticodec(options: { @@ -543,43 +542,6 @@ export class Jose { return { code, name }; } - public static joseToWebCrypto(options: - Partial - ): Web5Crypto.GenerateKeyOptions { - const params: string[] = []; - - /** - * All Elliptic Curve (EC) and Octet Key Pair (OKP) JSON Web Keys - * set a value for the "crv" parameter. - */ - if ('crv' in options && options.crv) { - params.push(options.crv); - // Special case for secp256k1. If alg is "ES256K", then ECDSA. Else ECDH. - if (options.crv === 'secp256k1' && options.alg === 'ES256K') { - params.push(options.alg); - } - - /** - * All Octet Sequence (oct) JSON Web Keys omit "crv" and - * set a value for the "alg" parameter. - */ - } else if (options.alg !== undefined) { - params.push(options.alg); - - } else { - throw new TypeError(`One or more parameters missing: 'alg' or 'crv'`); - } - - const lookupKey = params.join(':'); - const webCrypto = joseToWebCryptoMapping[lookupKey]; - - if (webCrypto === undefined) { - throw new Error(`Unsupported JOSE to WebCrypto conversion: '${lookupKey}'`); - } - - return { ...webCrypto }; - } - /** * Computes the thumbprint of a JSON Web Key (JWK) using the method * specified in RFC 7638. This function accepts RSA, EC, OKP, and oct keys @@ -662,197 +624,62 @@ export class Jose { return thumbprint; } - public static async jwkToCryptoKey(options: { - key: JsonWebKey - }): Promise { - const jsonWebKey = options.key; - - const { keyMaterial, keyType } = await Jose.jwkToKey({ key: jsonWebKey }); - - // Translate JOSE format to WebCrypto algorithm. - let algorithm = Jose.joseToWebCrypto(jsonWebKey) as Web5Crypto.GenerateKeyOptions; - - // Set extractable parameter. - let extractable: boolean; - if ('ext' in jsonWebKey && jsonWebKey.ext !== undefined) { - extractable = jsonWebKey.ext === 'true' ? true : false; - } else { - throw new Error(`Conversion from JWK to CryptoKey failed. Required parameter missing: 'ext'`); - } - - // Set key use parameter. - let keyUsage: Web5Crypto.KeyUsage[]; - if ('key_ops' in jsonWebKey && jsonWebKey.key_ops !== undefined) { - keyUsage = jsonWebKey.key_ops as Web5Crypto.KeyUsage[]; - } else { - throw new Error(`Conversion from JWK to CryptoKey failed. Required parameter missing: 'key_ops'`); - } - - const cryptoKey = new CryptoKey( - algorithm, - extractable, - keyMaterial, - keyType, - keyUsage - ); - - return cryptoKey; - } - - public static async jwkToKey(options: { - key: JsonWebKey - }): Promise<{ keyMaterial: Uint8Array, keyType: Web5Crypto.KeyType }> { - const jsonWebKey = options.key; - - let keyMaterial: Uint8Array; - let keyType: Web5Crypto.KeyType; - - // Asymmetric private key ("EC" or "OKP" - Curve25519 or SECG curves). - if ('d' in jsonWebKey) { - keyMaterial = Convert.base64Url(jsonWebKey.d).toUint8Array(); - keyType = 'private'; - } - - // Asymmetric public key ("EC" - secp256k1, secp256r1, secp384r1, secp521r1). - else if ('y' in jsonWebKey && jsonWebKey.y) { - const prefix = new Uint8Array([0x04]); // Designates an uncompressed key. - const x = Convert.base64Url(jsonWebKey.x).toUint8Array(); - const y = Convert.base64Url(jsonWebKey.y).toUint8Array(); - - const publicKey = new Uint8Array([...prefix, ...x, ...y]); - keyMaterial = publicKey; - keyType = 'public'; - } - - // Asymmetric public key ("OKP" - Ed25519, X25519). - else if ('x' in jsonWebKey) { - keyMaterial = Convert.base64Url(jsonWebKey.x).toUint8Array(); - keyType = 'public'; - } - - // Symmetric encryption or signature key ("oct" - AES, HMAC) - else if ('k' in jsonWebKey) { - keyMaterial = Convert.base64Url(jsonWebKey.k).toUint8Array(); - keyType = 'private'; - } - - else { - throw new Error('Jose: Unknown JSON Web Key format.'); - } - - return { keyMaterial, keyType }; - } - /** - * Note: All secp public keys are converted to compressed point encoding - * before the multibase identifier is computed. - * - * Per {@link https://github.com/multiformats/multicodec/blob/master/table.csv | Multicodec table}: - * public keys for Elliptic Curve cryptography algorithms (e.g., secp256k1, - * secp256k1r1, secp384r1, etc.) are always represented with compressed point - * encoding (e.g., secp256k1-pub, p256-pub, p384-pub, etc.). - * - * Per {@link https://datatracker.ietf.org/doc/html/rfc8812#name-jose-and-cose-secp256k1-cur | RFC 8812}: - * "As a compressed point encoding representation is not defined for JWK - * elliptic curve points, the uncompressed point encoding defined there - * MUST be used. The x and y values represented MUST both be exactly - * 256 bits, with any leading zeros preserved. - * - */ - public static async jwkToMultibaseId(options: { - key: JsonWebKey + * Note: All secp public keys are converted to compressed point encoding + * before the multibase identifier is computed. + * + * Per {@link https://github.com/multiformats/multicodec/blob/master/table.csv | Multicodec table}: + * Public keys for Elliptic Curve cryptography algorithms (e.g., secp256k1, + * secp256k1r1, secp384r1, etc.) are always represented with compressed point + * encoding (e.g., secp256k1-pub, p256-pub, p384-pub, etc.). + * + * Per {@link https://datatracker.ietf.org/doc/html/rfc8812#name-jose-and-cose-secp256k1-cur | RFC 8812}: + * "As a compressed point encoding representation is not defined for JWK + * elliptic curve points, the uncompressed point encoding defined there + * MUST be used. The x and y values represented MUST both be exactly + * 256 bits, with any leading zeros preserved." + */ + public static async publicKeyToMultibaseId(options: { + publicKey: PublicKeyJwk }): Promise { - const jsonWebKey = options.key; + const { publicKey } = options; - // Convert the algorithm into Multicodec name format. - const { name: multicodecName } = await Jose.joseToMulticodec({ key: jsonWebKey }); - - // Decode the key as a raw binary data from the JWK. - let { keyMaterial } = await Jose.jwkToKey({ key: jsonWebKey }); - - // Convert secp256k1 public keys to compressed format. - if ('crv' in jsonWebKey && !('d' in jsonWebKey)) { - switch (jsonWebKey.crv) { - case 'secp256k1': { - keyMaterial = await Secp256k1.convertPublicKey({ - publicKey : keyMaterial, - compressedPublicKey : true - }); - break; - } - } + if (!('crv' in publicKey)) { + throw new Error(`Jose: Unsupported public key type: ${publicKey.kty}`); } - // Compute the multibase identifier based on the provided key. - const multibaseId = keyToMultibaseId({ key: keyMaterial, multicodecName }); + let publicKeyBytes: Uint8Array; - return multibaseId; - } - - public static async keyToJwk(options: - Partial & { - keyMaterial: Uint8Array, - keyType: Web5Crypto.KeyType, - }): Promise { - const { keyMaterial, keyType, ...jsonWebKeyOptions } = options; + switch (publicKey.crv) { + case 'Ed25519': { + publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); + break; + } - let jsonWebKey = { ...jsonWebKeyOptions } as JsonWebKey; + case 'secp256k1': { + publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey }); + // Convert secp256k1 public keys to compressed format. + publicKeyBytes = await Secp256k1.compressPublicKey({ publicKeyBytes }); + break; + } - /** - * All Elliptic Curve (EC) and Octet Key Pair (OKP) keys - * specify a "crv" (named curve) parameter. - */ - if ('crv' in jsonWebKey) { - switch (jsonWebKey.crv) { - - case 'Ed25519': { - const publicKey = (keyType === 'private') - ? await Ed25519.getPublicKey({ privateKey: keyMaterial }) - : keyMaterial; - jsonWebKey.x = Convert.uint8Array(publicKey).toBase64Url(); - jsonWebKey.kty ??= 'OKP'; - break; - } - - case 'X25519': { - const publicKey = (keyType === 'private') - ? await X25519.getPublicKey({ privateKey: keyMaterial }) - : keyMaterial; - jsonWebKey.x = Convert.uint8Array(publicKey).toBase64Url(); - jsonWebKey.kty ??= 'OKP'; - break; - } - - case 'secp256k1': { - const points = await Secp256k1.getCurvePoints({ key: keyMaterial }); - jsonWebKey.x = Convert.uint8Array(points.x).toBase64Url(); - jsonWebKey.y = Convert.uint8Array(points.y).toBase64Url(); - jsonWebKey.kty ??= 'EC'; - break; - } - - default: { - throw new Error(`Unsupported key to JWK conversion: ${jsonWebKey.crv}`); - } + case 'X25519': { + publicKeyBytes = await X25519.publicKeyToBytes({ publicKey }); + break; } - if (keyType === 'private') { - jsonWebKey = { - d: Convert.uint8Array(keyMaterial).toBase64Url(), - ...jsonWebKey - }; + default: { + throw new Error(`Jose: Unsupported public key curve: ${publicKey.crv}`); } } - /** - * All Octet Sequence (oct) symmetric encryption and signature keys - * specify only an "alg" parameter. - */ - if (!('crv' in jsonWebKey) && jsonWebKey.kty === 'oct') { - jsonWebKey.k = Convert.uint8Array(keyMaterial).toBase64Url(); - } + // Convert the JSON Web Key (JWK) parameters to a Multicodec name. + const { name: multicodecName } = await Jose.joseToMulticodec({ key: publicKey }); + + // Compute the multibase identifier based on the provided key. + const multibaseId = keyToMultibaseId({ key: publicKeyBytes, multicodecName }); - return { ...jsonWebKey }; + return multibaseId; } public static async multicodecToJose(options: { @@ -879,51 +706,6 @@ export class Jose { return { ...jose }; } - public static webCryptoToJose(options: - Web5Crypto.Algorithm | Web5Crypto.GenerateKeyOptions - ): Partial { - const params: string[] = []; - - /** - * All WebCrypto algorithms have the "named" parameter. - */ - params.push(options.name); - - /** - * All Elliptic Curve (EC) WebCrypto algorithms - * set a value for the "namedCurve" parameter. - */ - if ('namedCurve' in options) { - params.push(options.namedCurve); - - /** - * All symmetric encryption (AES) WebCrypto algorithms - * set a value for the "length" parameter. - */ - } else if ('length' in options && options.length !== undefined) { - params.push(options.length.toString()); - - /** - * All symmetric signature (HMAC) WebCrypto algorithms - * set a value for the "hash" parameter. - */ - } else if ('hash' in options) { - params.push(options.hash.name); - - } else { - throw new TypeError(`One or more parameters missing: 'name', 'namedCurve', 'length', or 'hash'`); - } - - const lookupKey = params.join(':'); - const jose = webCryptoToJoseMapping[lookupKey]; - - if (jose === undefined) { - throw new Error(`Unsupported WebCrypto to JOSE conversion: '${lookupKey}'`); - } - - return { ...jose }; - } - private static canonicalize(obj: { [key: string]: any }): string { const sortedKeys = Object.keys(obj).sort(); const sortedObj = sortedKeys.reduce<{ [key: string]: any }>((acc, key) => { @@ -932,17 +714,4 @@ export class Jose { }, {}); return JSON.stringify(sortedObj); } -} - -type Constructable = new (...args: any[]) => object; - -export function CryptoKeyToJwkMixin(Base: T) { - return class extends Base { - public async toJwk(): Promise { - const jwk = Jose.cryptoKeyToJwk({ key: (this as unknown) as CryptoKey }); - return jwk; - } - }; -} - -export const CryptoKeyWithJwk = CryptoKeyToJwkMixin(CryptoKey); \ No newline at end of file +} \ No newline at end of file diff --git a/packages/crypto/src/types/crypto-key.ts b/packages/crypto/src/types/crypto-key.ts deleted file mode 100644 index 864fc68cf..000000000 --- a/packages/crypto/src/types/crypto-key.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface BytesKeyPair { - privateKey: Uint8Array; - publicKey: Uint8Array; -} \ No newline at end of file diff --git a/packages/crypto/src/types/web5-crypto.ts b/packages/crypto/src/types/web5-crypto.ts index 55d017009..60d789de0 100644 --- a/packages/crypto/src/types/web5-crypto.ts +++ b/packages/crypto/src/types/web5-crypto.ts @@ -1,12 +1,12 @@ +import type { PublicKeyJwk } from '../jose.js'; + export namespace Web5Crypto { export interface AesCtrOptions extends Algorithm { counter: Uint8Array; length: number; } - export interface AesGenerateKeyOptions extends Algorithm { - length: number; - } + export interface AesGenerateKeyOptions extends Algorithm { } export interface AesGcmOptions extends Algorithm { additionalData?: Uint8Array; @@ -20,34 +20,16 @@ export namespace Web5Crypto { export type AlgorithmIdentifier = Algorithm; - export interface CryptoKey { - algorithm: Web5Crypto.Algorithm; - extractable: boolean; - material: Uint8Array; - type: KeyType; - usages: KeyUsage[]; - } - - export interface CryptoKeyPair { - privateKey: CryptoKey; - publicKey: CryptoKey; - } - - export interface EcdsaOptions extends Algorithm { - hash: string; - } + export interface EcdsaOptions extends Algorithm {} export interface EcGenerateKeyOptions extends Algorithm { - namedCurve: NamedCurve; + curve: NamedCurve; } - export interface EcdhDeriveKeyOptions extends Algorithm { - publicKey: CryptoKey; + publicKey: PublicKeyJwk; } - export interface EcdsaGenerateKeyOptions extends EcGenerateKeyOptions { - compressedPublicKey?: boolean; - } + export interface EcdsaGenerateKeyOptions extends EcGenerateKeyOptions { } export type EdDsaGenerateKeyOptions = EcGenerateKeyOptions @@ -66,46 +48,6 @@ export namespace Web5Crypto { export type KeyFormat = 'jwk' | 'pkcs8' | 'raw' | 'spki'; - export interface KeyPairUsage { - privateKey: KeyUsage[]; - publicKey: KeyUsage[]; - } - - /** - * KeyType - * - * The read-only `type` property indicates which kind of key - * is represented by the object. - * - * It can have the following string values: - * - * "secret": This key is a secret key for use with a symmetric algorithm. - * "private": This key is the private half of an asymmetric algorithm's `ManagedKeyPair`. - * "public": This key is the public half of an asymmetric algorithm's `ManagedKeyPair`. - */ - export type KeyType = 'private' | 'public' | 'secret'; - - /** - * KeyUsage - * - * The read-only usage property indicates what can be done with the key. - * - * An Array of strings from the following list: - * - * "encrypt": The key may be used to encrypt messages. - * "decrypt": The key may be used to decrypt messages. - * "sign": The key may be used to sign messages. - * "verify": The key may be used to verify signatures. - * "deriveKey": The key may be used in deriving a new key. - * "deriveBits": The key may be used in deriving bits. - * "wrapKey": The key may be used to wrap a key. - * "unwrapKey": The key may be used to unwrap a key. - * - * Reference: IANA "JSON Web Key Operations" registry - * https://www.iana.org/assignments/jose/jose.xhtml#web-key-operations - */ - export type KeyUsage = 'encrypt' | 'decrypt' | 'sign' | 'verify' | 'deriveKey' | 'deriveBits' | 'wrapKey' | 'unwrapKey'; - export type NamedCurve = string; export interface Pbkdf2Options extends Algorithm { @@ -113,6 +55,4 @@ export namespace Web5Crypto { iterations: number; salt: Uint8Array; } - - export type PrivateKeyType = 'private' | 'secret'; } \ No newline at end of file diff --git a/packages/crypto/src/utils.ts b/packages/crypto/src/utils.ts index 0c448f099..2636f815a 100644 --- a/packages/crypto/src/utils.ts +++ b/packages/crypto/src/utils.ts @@ -1,8 +1,4 @@ -// import type { BytesKeyPair, Web5Crypto } from './types/index.js'; -import type { Web5Crypto } from './types/web5-crypto.js'; -import type { BytesKeyPair } from './types/crypto-key.js'; - -import { Convert, Multicodec, universalTypeOf } from '@web5/common'; +import { Convert, Multicodec } from '@web5/common'; import { bytesToHex, randomBytes as nobleRandomBytes } from '@noble/hashes/utils'; /** @@ -35,7 +31,7 @@ export function checkRequiredProperty(options: { * @throws {SyntaxError} If the property is not a member of the allowedProperties Array, Map, or Set. */ export function checkValidProperty(options: { - property: string, allowedProperties: Array | Map | Set + property: string, allowedProperties: ReadonlyArray | Array | Map | Set }): void { if (!options || options.property === undefined || options.allowedProperties === undefined) { throw new TypeError(`One or more required parameters missing: 'property, allowedProperties'`); @@ -51,30 +47,6 @@ export function checkValidProperty(options: { } } -/** - * Type guard function to check if the given key is a raw key pair - * of Uint8Array typed arrays. - * - * @param key The key to check. - * @returns True if the key is a pair of Uint8Array typed arrays, false otherwise. - */ -export function isBytesKeyPair(key: BytesKeyPair | undefined): key is BytesKeyPair { - return (key && 'privateKey' in key && 'publicKey' in key && - universalTypeOf(key.privateKey) === 'Uint8Array' && - universalTypeOf(key.publicKey) === 'Uint8Array') ? true : false; -} - -/** - * Type guard function to check if the given key is a - * Web5Crypto.CryptoKeyPair. - * - * @param key The key to check. - * @returns True if the key is a CryptoKeyPair, false otherwise. - */ -export function isCryptoKeyPair(key: Web5Crypto.CryptoKey | Web5Crypto.CryptoKeyPair): key is Web5Crypto.CryptoKeyPair { - return key && 'privateKey' in key && 'publicKey' in key; -} - export function keyToMultibaseId(options: { key: Uint8Array, multicodecCode?: number, diff --git a/packages/crypto/tests/algorithms-api.spec.ts b/packages/crypto/tests/algorithms-api.spec.ts index ae0d361c5..6c946f079 100644 --- a/packages/crypto/tests/algorithms-api.spec.ts +++ b/packages/crypto/tests/algorithms-api.spec.ts @@ -1,10 +1,20 @@ -import type { Web5Crypto } from '../src/types/web5-crypto.js'; - import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; import chaiAsPromised from 'chai-as-promised'; +import type { Web5Crypto } from '../src/types/web5-crypto.js'; +import type { + JwkType, + JwkOperation, + PublicKeyJwk, + PrivateKeyJwk, + JwkParamsEcPublic, + JwkParamsEcPrivate, + JwkParamsOkpPublic, + JwkParamsOctPrivate, +} from '../src/jose.js'; + import { - CryptoKey, OperationError, CryptoAlgorithm, BaseAesAlgorithm, @@ -24,8 +34,8 @@ describe('Algorithms API', () => { describe('CryptoAlgorithm', () => { class TestCryptoAlgorithm extends CryptoAlgorithm { - public name = 'TestAlgorithm'; - public keyUsages: KeyUsage[] = ['decrypt', 'deriveBits', 'deriveKey', 'encrypt', 'sign', 'unwrapKey', 'verify', 'wrapKey']; + public names = ['TestAlgorithm'] as const; + public keyOperations: JwkOperation[] = ['decrypt', 'deriveBits', 'deriveKey', 'encrypt', 'sign', 'unwrapKey', 'verify', 'wrapKey']; public async decrypt(): Promise { return null as any; } @@ -35,8 +45,8 @@ describe('Algorithms API', () => { public async encrypt(): Promise { return null as any; } - public async generateKey(): Promise { - return { publicKey: {} as any, privateKey: {} as any }; + public async generateKey(): Promise { + return null as any; } public async sign(): Promise { return null as any; @@ -70,33 +80,6 @@ describe('Algorithms API', () => { }); }); - describe('checkCryptoKey()', () => { - it('does not throw with a valid CryptoKey object', () => { - const mockCryptoKey = { - algorithm : null, - extractable : null, - type : null, - usages : null - }; - expect(() => alg.checkCryptoKey({ - // @ts-expect-error because 'material' property is intentionally omitted to support WebCrypto API CryptoKeys. - key: mockCryptoKey - })).to.not.throw(); - }); - - it('throws an error if the algorithm name does not match', () => { - const mockCryptoKey = { - algorithm : null, - type : null, - usages : null - }; - expect(() => alg.checkCryptoKey({ - // @ts-expect-error because 'extractable' property is intentionally ommitted to trigger check to throw. - key: mockCryptoKey - })).to.throw(TypeError, 'Object is not a CryptoKey'); - }); - }); - describe('checkKeyAlgorithm()', () => { it('throws an error when keyAlgorithmName is undefined', async () => { expect(() => alg.checkKeyAlgorithm({} as any)).to.throw(TypeError, 'Required parameter missing'); @@ -104,11 +87,11 @@ describe('Algorithms API', () => { it('throws an error when keyAlgorithmName does not match', async () => { const wrongName = 'wrongName'; - expect(() => alg.checkKeyAlgorithm({ keyAlgorithmName: wrongName })).to.throw(InvalidAccessError, `Algorithm '${alg.name}' does not match the provided '${wrongName}' key.`); + expect(() => alg.checkKeyAlgorithm({ keyAlgorithmName: wrongName })).to.throw(InvalidAccessError, `Algorithm '${alg.names.join(', ')}' does not match the provided '${wrongName}' key.`); }); it('does not throw an error when keyAlgorithmName matches', async () => { - const correctName = alg.name; + const [ correctName ] = alg.names; expect(() => alg.checkKeyAlgorithm({ keyAlgorithmName: correctName })).not.to.throw(); }); }); @@ -121,112 +104,159 @@ describe('Algorithms API', () => { }); it('throws an error when keyType does not match allowedKeyType', async () => { - const keyType = 'public'; - const allowedKeyType = 'private'; - expect(() => alg.checkKeyType({ keyType, allowedKeyType })).to.throw(InvalidAccessError, 'Requested operation is not valid'); + const keyType: JwkType = 'oct'; + const allowedKeyTypes: JwkType[] = ['OKP']; + expect(() => alg.checkKeyType({ keyType, allowedKeyTypes })).to.throw(InvalidAccessError, 'Key type of the provided key must be'); + }); + + it('throws an error when allowedKeyTypes is not an array', () => { + expect( + () => alg.checkKeyType({ + keyType : 'oct', + allowedKeyTypes : {} as any // Intentionally incorrect type + }) + ).to.throw(TypeError, `'allowedKeyTypes' is not of type Array.`); }); it('does not throw an error when keyType matches allowedKeyType', async () => { - const keyType = 'public'; - const allowedKeyType = 'public'; - expect(() => alg.checkKeyType({ keyType, allowedKeyType })).not.to.throw(); + const keyType: JwkType = 'EC'; + const allowedKeyTypes: JwkType[] = ['EC']; + expect(() => alg.checkKeyType({ keyType, allowedKeyTypes })).not.to.throw(); }); }); - describe('checkKeyUsages()', () => { - it('throws an error when keyUsages is undefined or empty', async () => { - expect(() => alg.checkKeyUsages({ allowedKeyUsages: ['sign'] } as any)).to.throw(TypeError, 'Required parameter missing or empty'); - expect(() => alg.checkKeyUsages({ keyUsages: [], allowedKeyUsages: ['sign'] })).to.throw(TypeError, 'Required parameter missing or empty'); + describe('checkKeyOperations()', () => { + it('does not throw an error when keyOperations are in allowedKeyOperations', async () => { + const keyOperations: JwkOperation[] = ['sign', 'verify']; + const allowedKeyOperations: JwkOperation[] = ['sign', 'verify', 'encrypt', 'decrypt']; + expect(() => alg.checkKeyOperations({ keyOperations, allowedKeyOperations })).not.to.throw(); }); - it('throws an error when keyUsages are not in allowedKeyUsages', async () => { - const keyUsages: Web5Crypto.KeyUsage[] = ['encrypt', 'decrypt']; - const allowedKeyUsages: Web5Crypto.KeyUsage[] = ['sign', 'verify']; - expect(() => alg.checkKeyUsages({ keyUsages, allowedKeyUsages })).to.throw(InvalidAccessError, 'is not valid for the provided key'); - - const keyPairUsages: Web5Crypto.KeyPairUsage = { privateKey: ['sign'], publicKey: ['verify'] }; - expect(() => alg.checkKeyUsages({ keyUsages, allowedKeyUsages: keyPairUsages })).to.throw(InvalidAccessError, 'is not valid for the provided key'); + it('throws an error when keyOperations is undefined or empty', async () => { + expect(() => alg.checkKeyOperations({ allowedKeyOperations: ['sign'] } as any)).to.throw(TypeError, 'Required parameter missing or empty'); + expect(() => alg.checkKeyOperations({ keyOperations: [], allowedKeyOperations: ['sign'] })).to.throw(TypeError, 'Required parameter missing or empty'); }); - it('does not throw an error when keyUsages are in allowedKeyUsages', async () => { - const keyUsages: Web5Crypto.KeyUsage[] = ['sign', 'verify']; - const allowedKeyUsages: Web5Crypto.KeyUsage[] = ['sign', 'verify', 'encrypt', 'decrypt']; - expect(() => alg.checkKeyUsages({ keyUsages, allowedKeyUsages })).not.to.throw(); + it('throws an error when keyOperations are not in allowedKeyOperations', async () => { + const keyOperations: JwkOperation[] = ['encrypt', 'decrypt']; + const allowedKeyOperations: JwkOperation[] = ['sign', 'verify']; + expect(() => alg.checkKeyOperations({ keyOperations, allowedKeyOperations })).to.throw(InvalidAccessError, 'is not valid for the provided key'); + }); - const keyPairUsages: Web5Crypto.KeyPairUsage = { privateKey: ['sign'], publicKey: ['verify'] }; - expect(() => alg.checkKeyUsages({ keyUsages, allowedKeyUsages: keyPairUsages })).to.not.throw(); + it('throws an error when allowedKeyOperations is not an Array', async () => { + const keyOperations: JwkOperation[] = ['encrypt', 'decrypt']; + const allowedKeyOperations = 'sign' as any; // Intentionally incorrect type'; + expect(() => alg.checkKeyOperations({ keyOperations, allowedKeyOperations })).to.throw(TypeError, 'is not of type Array'); }); }); }); describe('BaseAesAlgorithm', () => { class TestAesAlgorithm extends BaseAesAlgorithm { - public name = 'TestAlgorithm'; - public keyUsages: KeyUsage[] = ['decrypt', 'encrypt']; + public names = ['TestAlgorithm'] as const; + public keyOperations: JwkOperation[] = ['decrypt', 'encrypt']; public async decrypt(): Promise { return null as any; } public async encrypt(): Promise { return null as any; } - public async generateKey(): Promise { + public async generateKey(): Promise { return null as any; } } - describe('checkGenerateKey()', () => { - let alg: TestAesAlgorithm; + let alg: TestAesAlgorithm; - beforeEach(() => { - alg = TestAesAlgorithm.create(); - }); + beforeEach(() => { + alg = TestAesAlgorithm.create(); + }); - it('does not throw with supported algorithm, length, and key usage', () => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'TestAlgorithm', length: 128 }, - keyUsages : ['encrypt'] + describe('checkGenerateKeyOptions()', () => { + it('does not throw with supported algorithm and key operation', () => { + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'TestAlgorithm' }, + keyOperations : ['encrypt'] })).to.not.throw(); }); it('throws an error when unsupported algorithm specified', () => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'ECDSA', length: 128 }, - keyUsages : ['encrypt'] + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'ECDSA' }, + keyOperations : ['encrypt'] })).to.throw(NotSupportedError, 'Algorithm not supported'); }); - it('throws an error when the length property is missing', () => { - expect(() => alg.checkGenerateKey({ - // @ts-expect-error because length was intentionally omitted. - algorithm : { name: 'TestAlgorithm' }, - keyUsages : ['encrypt'] - })).to.throw(TypeError, 'Required parameter missing'); + it('throws an error when the requested operation is not valid', () => { + ['sign', 'verify'].forEach((operation) => { + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'TestAlgorithm' }, + keyOperations : [operation as JwkOperation] + })).to.throw(InvalidAccessError, 'Requested operation'); + }); }); + }); + + describe('checkSecretKey()', () => { + let dataEncryptionKey: PrivateKeyJwk; - it('throws an error when the specified length is not a Number', () => { - expect(() => alg.checkGenerateKey({ - // @ts-expect-error because length is intentionally set as a string instead of number. - algorithm : { name: 'TestAlgorithm', length: '256' }, - keyUsages : ['encrypt'] - })).to.throw(TypeError, `is not of type: Number`); + beforeEach(() => { + dataEncryptionKey = { kty: 'oct', k: Convert.uint8Array(new Uint8Array(16)).toBase64Url() }; }); - it('throws an error when the specified length is not valid', () => { - [64, 96, 160, 224, 512].forEach((length) => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'TestAlgorithm', length }, - keyUsages : ['encrypt'] - })).to.throw(OperationError, `Algorithm 'length' must be 128, 192, or 256`); - }); + it('does not throw with a valid secret key', () => { + const key: PrivateKeyJwk = { + kty : 'oct', + k : Convert.uint8Array(new Uint8Array(16)).toBase64Url() + }; + expect(() => alg.checkSecretKey({ key })).to.not.throw(); }); - it('throws an error when the requested operation is not valid', () => { - ['sign', 'verify'].forEach((operation) => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'TestAlgorithm', length: 128 }, - keyUsages : [operation as KeyUsage] - })).to.throw(InvalidAccessError, 'Requested operation'); - }); + it('throws an error when the key is not a JWK', () => { + const key = 'foo' as any; // Intentionally incorrect type. + expect(() => alg.checkSecretKey({ key })).to.throw(TypeError, 'is not a JSON Web Key'); + }); + + it('throws an error if the key property is missing', () => { + // @ts-expect-error because key property was intentionally omitted. + expect(() => alg.checkSecretKey({})).to.throw(TypeError, `Required parameter missing: 'key'`); + }); + + it('throws an error if the given key is not valid', () => { + const { kty, ...keyMissingKeyType } = dataEncryptionKey as JwkParamsOctPrivate; + expect(() => alg.checkSecretKey({ + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + key: keyMissingKeyType + })).to.throw(TypeError, 'Object is not a JSON Web Key'); + + const { k, ...keyMissingK } = dataEncryptionKey as JwkParamsOctPrivate; + expect(() => alg.checkSecretKey({ + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + key: keyMissingK + })).to.throw(InvalidAccessError, 'Requested operation is only valid for oct private keys'); + }); + + it('if specified, throws an error if the algorithm of the key does not match', () => { + // @ts-expect-error because alg property is intentionally set to an invalid value. + const key: PrivateKeyJwk = { ...dataEncryptionKey, alg: 'invalid-alg' }; + expect(() => alg.checkSecretKey({ + key + })).to.throw(InvalidAccessError, 'does not match'); + }); + + it('throws an error if an EC private key is specified as the key', () => { + const secp256k1PrivateKey: PrivateKeyJwk = { kty: 'EC', crv: 'secp256k1', d: '', x: '', y: '' }; + expect(() => alg.checkSecretKey({ + key: secp256k1PrivateKey + })).to.throw(InvalidAccessError, 'operation is only valid'); + }); + + it('throws an error if a public key is specified as the key', () => { + const secp256k1PublicKey: PublicKeyJwk = { kty: 'EC', crv: 'secp256k1', x: '', y: '' }; + expect(() => alg.checkSecretKey({ + // @ts-expect-error because a public key is being intentionally specified as the key. + key: secp256k1PublicKey + })).to.throw(InvalidAccessError, 'operation is only valid'); }); }); @@ -239,191 +269,196 @@ describe('Algorithms API', () => { describe('sign()', () => { it(`throws an error because 'sign' operation is valid for AES-CTR keys`, async () => { - const alg = TestAesAlgorithm.create(); await expect(alg.sign()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); }); }); describe('verify()', () => { it(`throws an error because 'verify' operation is valid for AES-CTR keys`, async () => { - const alg = TestAesAlgorithm.create(); await expect(alg.verify()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); }); }); + }); - describe('BaseAesCtrAlgorithm', () => { - let alg: BaseAesCtrAlgorithm; + describe('BaseAesCtrAlgorithm', () => { + let alg: BaseAesCtrAlgorithm; - before(() => { - alg = Reflect.construct(BaseAesCtrAlgorithm, []) as BaseAesCtrAlgorithm; + before(() => { + alg = Reflect.construct(BaseAesCtrAlgorithm, []) as BaseAesCtrAlgorithm; + // @ts-expect-error because the `names` property is readonly. + alg.names = ['A128CTR', 'A192CTR', 'A256CTR'] as const; + }); + + describe('checkAlgorithmOptions()', () => { + it('does not throw with matching algorithm name and valid counter and length', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'A128CTR', + counter : new Uint8Array(16), + length : 128 + } + })).to.not.throw(); }); - let dataEncryptionKey: Web5Crypto.CryptoKey; + it('throws an error when unsupported algorithm specified', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'invalid-name', + counter : new Uint8Array(16), + length : 128 + } + })).to.throw(NotSupportedError, 'Algorithm not supported'); + }); - beforeEach(() => { - dataEncryptionKey = new CryptoKey({ name: 'AES-CTR', length: 128 }, false, new Uint8Array(32), 'secret', ['encrypt', 'decrypt']); - }); - - describe('checkAlgorithmOptions()', () => { - it('does not throw with matching algorithm name and valid counter and length', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm: { - name : 'AES-CTR', - counter : new Uint8Array(16), - length : 128 - }, - key: dataEncryptionKey - })).to.not.throw(); - }); + it('throws an error if the counter property is missing', () => { + // @ts-expect-error because `counter` property is intentionally omitted. + expect(() => alg.checkAlgorithmOptions({ algorithm: { + name : 'A128CTR', + length : 128 + }})).to.throw(TypeError, 'Required parameter missing'); + }); - it('throws an error when unsupported algorithm specified', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm: { - name : 'invalid-name', - counter : new Uint8Array(16), - length : 128 - }, - key: dataEncryptionKey - })).to.throw(NotSupportedError, 'Algorithm not supported'); - }); + it('accepts counter as Uint8Array', () => { + const data = new Uint8Array(16); + const algorithm: { name?: string, counter?: any, length?: number } = {}; + algorithm.name = 'A128CTR'; + algorithm.length = 128; - it('throws an error if the counter property is missing', () => { - // @ts-expect-error because `counter` property is intentionally omitted. - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'AES-CTR', - length : 128 - }})).to.throw(TypeError, 'Required parameter missing'); - }); + // TypedArray - Uint8Array + algorithm.counter = data; + expect(() => alg.checkAlgorithmOptions({ + algorithm: algorithm as Web5Crypto.AesCtrOptions, + })).to.not.throw(); + }); - it('accepts counter as Uint8Array', () => { - const data = new Uint8Array(16); - const algorithm: { name?: string, counter?: any, length?: number } = {}; - algorithm.name = 'AES-CTR'; - algorithm.length = 128; - - // TypedArray - Uint8Array - algorithm.counter = data; - expect(() => alg.checkAlgorithmOptions({ - algorithm : algorithm as Web5Crypto.AesCtrOptions, - key : dataEncryptionKey - })).to.not.throw(); - }); + it('throws error if counter is not acceptable data type', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'A128CTR', + // @ts-expect-error because counter is being intentionally set to the wrong data type to trigger an error. + counter : new Set([...Array(16).keys()].map(n => n.toString(16))), + length : 128 + }, + })).to.throw(TypeError, 'is not of type'); + }); - it('throws error if counter is not acceptable data type', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm: { - name : 'AES-CTR', - // @ts-expect-error because counter is being intentionally set to the wrong data type to trigger an error. - counter : new Set([...Array(16).keys()].map(n => n.toString(16))), - length : 128 - }, - key: dataEncryptionKey - })).to.throw(TypeError, 'is not of type'); - }); + it('throws error if initial value of the counter block is not 16 bytes', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'A128CTR', + counter : new Uint8Array(128), + length : 128 + } + })).to.throw(OperationError, 'must have length'); + }); - it('throws error if initial value of the counter block is not 16 bytes', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm: { - name : 'AES-CTR', - counter : new Uint8Array(128), - length : 128 - }, - key: dataEncryptionKey - })).to.throw(OperationError, 'must have length'); - }); + it('throws an error if the length property is missing', () => { + // @ts-expect-error because lengthy property was intentionally omitted. + expect(() => alg.checkAlgorithmOptions({ algorithm: { + name : 'A128CTR', + counter : new Uint8Array(16) + }})).to.throw(TypeError, `Required parameter missing: 'length'`); + }); - it('throws an error if the length property is missing', () => { - // @ts-expect-error because lengthy property was intentionally omitted. - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'AES-CTR', - counter : new Uint8Array(16) - }})).to.throw(TypeError, `Required parameter missing: 'length'`); - }); + it('throws an error if length is not a Number', () => { + expect(() => alg.checkAlgorithmOptions({ algorithm: { + name : 'A128CTR', + counter : new Uint8Array(16), + // @ts-expect-error because length is being intentionally specified as a string instead of a number. + length : '128' + }})).to.throw(TypeError, 'is not of type'); + }); - it('throws an error if length is not a Number', () => { - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'AES-CTR', + it('throws an error if length is not between 1 and 128', () => { + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'A128CTR', counter : new Uint8Array(16), - // @ts-expect-error because length is being intentionally specified as a string instead of a number. - length : '128' - }})).to.throw(TypeError, 'is not of type'); - }); - - it('throws an error if length is not between 1 and 128', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm: { - name : 'AES-CTR', - counter : new Uint8Array(16), - length : 0 - }, - key: dataEncryptionKey - })).to.throw(OperationError, 'should be in the range'); - - expect(() => alg.checkAlgorithmOptions({ - algorithm: { - name : 'AES-CTR', - counter : new Uint8Array(16), - length : 256 - }, - key: dataEncryptionKey - })).to.throw(OperationError, 'should be in the range'); - }); + length : 0 + } + })).to.throw(OperationError, 'should be in the range'); - it('throws an error if the key property is missing', () => { - // @ts-expect-error because key property was intentionally omitted. - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'AES-CTR', + expect(() => alg.checkAlgorithmOptions({ + algorithm: { + name : 'A128CTR', counter : new Uint8Array(16), - length : 64 - }})).to.throw(TypeError, `Required parameter missing: 'key'`); - }); + length : 256 + } + })).to.throw(OperationError, 'should be in the range'); + }); + }); - it('throws an error if the given key is not valid', () => { - // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. - delete dataEncryptionKey.extractable; - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 64 }, - key : dataEncryptionKey - })).to.throw(TypeError, 'Object is not a CryptoKey'); - }); + describe('checkDecryptOptions()', () => { + let algorithm: Web5Crypto.AesCtrOptions; + let dataEncryptionKey: PrivateKeyJwk; - it('throws an error if the algorithm of the key does not match', () => { - const dataEncryptionKey = new CryptoKey({ name: 'non-existent-algorithm', length: 128 }, false, new Uint8Array(32), 'secret', ['encrypt', 'decrypt']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 64 }, - key : dataEncryptionKey - })).to.throw(InvalidAccessError, 'does not match'); - }); + beforeEach(() => { + algorithm = { name: 'A128CTR', counter: new Uint8Array(16), length: 128 }; + dataEncryptionKey = { kty: 'oct', k: Convert.uint8Array(new Uint8Array(16)).toBase64Url() }; + }); - it('throws an error if a private key is specified as the key', () => { - const dataEncryptionKey = new CryptoKey({ name: 'AES-CTR', length: 128 }, false, new Uint8Array(32), 'private', ['encrypt', 'decrypt']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 64 }, - key : dataEncryptionKey - })).to.throw(InvalidAccessError, 'Requested operation is not valid'); - }); + it('validates that data is a Uint8Array', async () => { + expect(() => alg.checkDecryptOptions({ + algorithm, + key : dataEncryptionKey, + // @ts-expect-error because invalid data type intentionally specified. + data : 'baz' + })).to.throw(TypeError, `data must be of type Uint8Array`); + }); - it('throws an error if a public key is specified as the key', () => { - const dataEncryptionKey = new CryptoKey({ name: 'AES-CTR', length: 128 }, false, new Uint8Array(32), 'public', ['encrypt', 'decrypt']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 64 }, - key : dataEncryptionKey - })).to.throw(InvalidAccessError, 'Requested operation is not valid'); - }); + it(`if specified, validates that 'key_opts' includes 'decrypt'`, async () => { + // Exclude the 'decrypt' operation. + dataEncryptionKey.key_ops = ['encrypt']; + + expect(() => alg.checkDecryptOptions({ + algorithm, + key : dataEncryptionKey, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(InvalidAccessError, 'is not valid for the provided key'); + }); + }); + + describe('checkEncryptOptions()', () => { + let algorithm: Web5Crypto.AesCtrOptions; + let dataEncryptionKey: PrivateKeyJwk; + + beforeEach(() => { + algorithm = { name: 'A128CTR', counter: new Uint8Array(16), length: 128 }; + dataEncryptionKey = { kty: 'oct', k: Convert.uint8Array(new Uint8Array(16)).toBase64Url() }; + }); + + it('validates that data is a Uint8Array', async () => { + expect(() => alg.checkEncryptOptions({ + algorithm, + key : dataEncryptionKey, + // @ts-expect-error because invalid data type intentionally specified. + data : 'baz' + })).to.throw(TypeError, `data must be of type Uint8Array`); + }); + + it(`if specified, validates that 'key_opts' includes 'encrypt'`, async () => { + // Exclude the 'encrypt' operation. + dataEncryptionKey.key_ops = ['decrypt']; + + expect(() => alg.checkEncryptOptions({ + algorithm, + key : dataEncryptionKey, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(InvalidAccessError, 'is not valid for the provided key'); }); }); }); describe('BaseEllipticCurveAlgorithm', () => { class TestEllipticCurveAlgorithm extends BaseEllipticCurveAlgorithm { - public name = 'TestAlgorithm'; - public namedCurves = ['curveA']; - public keyUsages: KeyUsage[] = ['decrypt']; + public names = ['TestAlgorithm'] as const; + public curves = ['secp256k1'] as const; + public keyOperations: JwkOperation[] = ['decrypt']; public async deriveBits(): Promise { return null as any; } - public async generateKey(): Promise { - return { publicKey: {} as any, privateKey: {} as any }; + public async generateKey(): Promise { + return null as any; } public async sign(): Promise { return null as any; @@ -433,252 +468,611 @@ describe('Algorithms API', () => { } } - describe('checkGenerateKey()', () => { + describe('checkGenerateKeyOptions()', () => { let alg: TestEllipticCurveAlgorithm; beforeEach(() => { alg = TestEllipticCurveAlgorithm.create(); }); - it('does not throw with supported algorithm, named curve, and key usage', () => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'TestAlgorithm', namedCurve: 'curveA' }, - keyUsages : ['decrypt'] + it('does not throw with supported algorithm, named curve, and key operation', () => { + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'TestAlgorithm', curve: 'secp256k1' }, + keyOperations : ['decrypt'] })).to.not.throw(); }); it('throws an error when unsupported algorithm specified', () => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - keyUsages : ['sign'] + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'invalid-algorithm', curve: 'secp256k1' }, + keyOperations : ['sign'] })).to.throw(NotSupportedError, 'Algorithm not supported'); }); it('throws an error when unsupported named curve specified', () => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'TestAlgorithm', namedCurve: 'X25519' }, - keyUsages : ['sign'] + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'TestAlgorithm', curve: 'invalid-curve' }, + keyOperations : ['sign'] })).to.throw(TypeError, 'Out of range'); }); it('throws an error when the requested operation is not valid', () => { ['sign', 'verify'].forEach((operation) => { - expect(() => alg.checkGenerateKey({ - algorithm : { name: 'TestAlgorithm', namedCurve: 'curveA' }, - keyUsages : [operation as KeyUsage] + expect(() => alg.checkGenerateKeyOptions({ + algorithm : { name: 'TestAlgorithm', curve: 'secp256k1' }, + keyOperations : [operation as JwkOperation] })).to.throw(InvalidAccessError, 'Requested operation'); }); }); }); + describe('checkSignOptions()', () => { + let alg: TestEllipticCurveAlgorithm; + + beforeEach(() => { + alg = TestEllipticCurveAlgorithm.create(); + }); + + it('validates algorithm name and key algorithm name', async () => { + // Invalid (algorithm name, private key) result in algorithm name check failing first. + expect(() => alg.checkSignOptions({ + algorithm : { name: 'invalid-name' }, + // @ts-expect-error because invalid key intentionally specified. + key : { foo: 'bar '}, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(NotSupportedError, 'Algorithm not supported'); + + // Valid (algorithm name) + Invalid (private key) result in private key check failing first. + expect(() => alg.checkSignOptions({ + algorithm : { name: 'TestAlgorithm' }, + // @ts-expect-error because invalid key intentionally specified. + key : { foo: 'bar '}, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(InvalidAccessError, 'operation is only valid for private keys'); + + // Valid (algorithm name) + Invalid (private key alg) result in private key algorithm check failing first. + expect(() => alg.checkSignOptions({ + algorithm : { name: 'TestAlgorithm' }, + // @ts-expect-error because invalid key algorithm intentionally specified. + key : { kty: 'EC', crv: 'secp256k1', d: '', x: '', y: '', alg: 'invalid-alg' }, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(InvalidAccessError, `does not match the provided 'invalid-alg' key`); + }); + + it('validates that data is a Uint8Array', async () => { + const privateKey: PrivateKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + d : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + x : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + y : Convert.uint8Array(new Uint8Array(32)).toBase64Url() + }; + + // Valid (algorithm name, private key) + Invalid (data) result in the data check failing first. + expect(() => alg.checkSignOptions({ + algorithm : { name: 'TestAlgorithm' }, + key : privateKey, + // @ts-expect-error because invalid data type intentionally specified. + data : 'baz' + })).to.throw(TypeError, `data must be of type Uint8Array`); + }); + + it('validates that key is not a public key', async () => { + const publicKey: PublicKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + y : Convert.uint8Array(new Uint8Array(32)).toBase64Url() + }; + + // Valid (algorithm name, data) + Invalid (private key) result in key type check failing first. + expect(() => alg.checkSignOptions({ + algorithm : { name: 'TestAlgorithm' }, + // @ts-expect-error because invalid key intentionally specified. + key : publicKey, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(InvalidAccessError, 'operation is only valid for private keys'); + }); + + it(`if specified, validates that 'key_opts' includes 'sign'`, async () => { + // Exclude the 'sign' operation. + const privateKey: PrivateKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + d : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + x : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + y : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + key_ops : ['verify'] + }; + + expect(() => alg.checkSignOptions({ + algorithm : { name: 'TestAlgorithm' }, + key : privateKey, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(InvalidAccessError, 'is not valid for the provided key'); + }); + + it('throws an error when key is an unsupported curve', async () => { + const privateKey: PrivateKeyJwk = { + kty : 'EC', + // @ts-expect-error because an invalid curve is being intentionally specified. + crv : 'invalid-curve', + d : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + x : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + y : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + key_ops : ['verify'] + }; + + expect(() => alg.checkSignOptions({ + algorithm : { name: 'TestAlgorithm' }, + key : privateKey, + data : new Uint8Array([1, 2, 3, 4]) + })).to.throw(TypeError, 'Out of range'); + }); + }); + + describe('checkVerifyOptions()', () => { + let alg: TestEllipticCurveAlgorithm; + let privateKey: PrivateKeyJwk; + let publicKey: PublicKeyJwk; + let signature: Uint8Array; + let data = new Uint8Array([51, 52, 53]); + + beforeEach(() => { + alg = TestEllipticCurveAlgorithm.create(); + + privateKey = { + kty : 'EC', + crv : 'secp256k1', + d : 'XwsSwwmtfxgooR2XsWsvZxeacO1W4koDw3iXxmUivcE', + x : 'Ldwc5EnadPCf-pXe_qWmM7i2-qfYrQXkSCm4aOJ09UQ', + y : 'vL7LbN7q072aRJ5TSpz63cOetIzEDmBR_LwKciPfHZE', + kid : 'ukuZTjeoTyhQk5pScZwj3PDHLUmMffmV5Fey4cS2sMk', + key_ops : [ 'sign' ] + }; + publicKey = { + kty : 'EC', + crv : 'secp256k1', + x : 'Ldwc5EnadPCf-pXe_qWmM7i2-qfYrQXkSCm4aOJ09UQ', + y : 'vL7LbN7q072aRJ5TSpz63cOetIzEDmBR_LwKciPfHZE', + kid : 'ukuZTjeoTyhQk5pScZwj3PDHLUmMffmV5Fey4cS2sMk', + key_ops : [ 'sign' ] + }; + signature = Convert.base64Url('jikTSNWducZQBBDCjonE-OnQaUc3A0oFnCcWWF5N2OV2AYID4iGSTrdPw9jgXISBhojZ1kYeeu4_6YvV26A6GQ').toUint8Array(); + }); + + it('validates algorithm name and key algorithm name', async () => { + // Invalid (algorithm name, public key) result in algorithm name check failing first. + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'invalid-name' }, + // @ts-expect-error because invalid key intentionally specified. + key : { foo: 'bar '}, + signature, + data + })).to.throw(NotSupportedError, 'Algorithm not supported'); + + // Valid (algorithm name) + Invalid (public key) result in public key check failing first. + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + // @ts-expect-error because invalid key intentionally specified. + key : { foo: 'bar '}, + signature, + data + })).to.throw(InvalidAccessError, 'operation is only valid for public keys'); + + // Valid (algorithm name) + Invalid (public key alg) result in public key algorithm check failing first. + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + // @ts-expect-error because invalid key intentionally specified. + key : { ...publicKey, alg: 'invalid-alg' }, + signature, + data + })).to.throw(InvalidAccessError, `does not match the provided 'invalid-alg' key`); + }); + + it('validates that key is not a private key', async () => { + // Valid (algorithm name, hash algorithm, signature, data) + Invalid (public key) result in key type check failing first. + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + // @ts-expect-error because invalid key intentionally specified. + key : privateKey, + signature : signature, + data : data + })).to.throw(InvalidAccessError, 'operation is only valid for public keys'); + }); + + it(`if specified, validates that 'key_ops' includes 'verify'`, async () => { + // Manually specify the public key operations to exclude the 'verify' operation. + const key: PublicKeyJwk = { ...publicKey, key_ops: ['sign'] }; + + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + key, + signature : signature, + data : data + })).to.throw(InvalidAccessError, 'is not valid for the provided key'); + }); + + it('throws an error when key is an unsupported curve', async () => { + // Manually change the key's curve to trigger an error. + // @ts-expect-error because an invalid curve is being intentionally specified. + const key: PublicKeyJwk = { ...publicKey, crv: 'invalid-curve' }; + + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + data : data, + key, + signature + })).to.throw(TypeError, 'Out of range'); + }); + + it('validates that data is a Uint8Array', async () => { + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + key : publicKey, + // @ts-expect-error because invalid data type intentionally specified. + data : 'baz', + signature + })).to.throw(TypeError, `data must be of type Uint8Array`); + }); + + it('validates that signature is a Uint8Array', async () => { + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'TestAlgorithm' }, + key : publicKey, + data, + // @ts-expect-error because invalid data type intentionally specified. + signature : 'baz' + })).to.throw(TypeError, `signature must be of type Uint8Array`); + }); + }); + describe('decrypt()', () => { - it(`throws an error because 'decrypt' operation is valid for AES-CTR keys`, async () => { + it(`throws an error because 'decrypt' operation is valid for Elliptic Curve algorithms`, async () => { const alg = TestEllipticCurveAlgorithm.create(); await expect(alg.decrypt()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); }); }); describe('encrypt()', () => { - it(`throws an error because 'encrypt' operation is valid for AES-CTR keys`, async () => { + it(`throws an error because 'encrypt' operation is valid for Elliptic Curve algorithms`, async () => { const alg = TestEllipticCurveAlgorithm.create(); await expect(alg.encrypt()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for'); }); }); + }); - describe('BaseEcdhAlgorithm', () => { - let alg: BaseEcdhAlgorithm; + describe('BaseEcdhAlgorithm', () => { + let alg: BaseEcdhAlgorithm; - before(() => { - alg = Reflect.construct(BaseEcdhAlgorithm, []) as BaseEcdhAlgorithm; - }); + before(() => { + alg = Reflect.construct(BaseEcdhAlgorithm, []) as BaseEcdhAlgorithm; + // @ts-expect-error because the `names` property is readonly. + alg.names = ['ECDH'] as const; + }); - describe('checkAlgorithmOptions()', () => { + describe('checkDeriveBitsOptions()', () => { + let otherPartyPublicKey: PublicKeyJwk; + let ownPrivateKey: PrivateKeyJwk; - let otherPartyPublicKey: Web5Crypto.CryptoKey; - let ownPrivateKey: Web5Crypto.CryptoKey; + beforeEach(() => { + otherPartyPublicKey = { + kty : 'OKP', + crv : 'X25519', + x : Convert.uint8Array(new Uint8Array(32)).toBase64Url() + }; + ownPrivateKey = { + kty : 'OKP', + crv : 'X25519', + x : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + d : Convert.uint8Array(new Uint8Array(32)).toBase64Url() + }; + }); - beforeEach(() => { - otherPartyPublicKey = new CryptoKey({ name: 'ECDH', namedCurve: 'X25519' }, false, new Uint8Array(32), 'public', ['deriveBits', 'deriveKey']); - ownPrivateKey = new CryptoKey({ name: 'ECDH', namedCurve: 'X25519' }, false, new Uint8Array(32), 'private', ['deriveBits', 'deriveKey']); - }); + it('does not throw with matching algorithm name and valid publicKey and baseKey', () => { + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.not.throw(); + }); - it('does not throw with matching algorithm name and valid publicKey and baseKey', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.not.throw(); - }); + it('throws an error when unsupported algorithm specified', () => { + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'non-existent-algorithm', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(NotSupportedError, 'Algorithm not supported'); + }); - it('throws an error when unsupported algorithm specified', () => { - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'non-existent-algorithm', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(NotSupportedError, 'Algorithm not supported'); - }); + it('throws an error if the publicKey property is missing', () => { + expect(() => alg.checkDeriveBitsOptions({ + // @ts-expect-error because `publicKey` property is intentionally omitted. + algorithm : { name: 'ECDH' }, + baseKey : ownPrivateKey + })).to.throw(TypeError, `Required parameter missing: 'publicKey'`); + }); - it('throws an error if the publicKey property is missing', () => { - expect(() => alg.checkAlgorithmOptions({ - // @ts-expect-error because `publicKey` property is intentionally omitted. - algorithm : { name: 'ECDH' }, - baseKey : ownPrivateKey - })).to.throw(TypeError, `Required parameter missing: 'publicKey'`); - }); + it('throws an error if the given publicKey is not valid', () => { + const { kty, ...otherPartyPublicKeyMissingKeyType } = otherPartyPublicKey as JwkParamsEcPublic; + expect(() => alg.checkDeriveBitsOptions({ + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKeyMissingKeyType }, + baseKey : ownPrivateKey + })).to.throw(TypeError, 'Object is not a JSON Web Key'); - it('throws an error if the given publicKey is not valid', () => { + const { crv, ...otherPartyPublicKeyMissingCurve } = otherPartyPublicKey as JwkParamsEcPublic; + expect(() => alg.checkDeriveBitsOptions({ // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. - delete otherPartyPublicKey.extractable; - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(TypeError, 'Object is not a CryptoKey'); - }); + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKeyMissingCurve }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, 'Requested operation is only valid for public keys'); - it('throws an error if the algorithm of the publicKey does not match', () => { - const otherPartyPublicKey = new CryptoKey({ name: 'Nope', namedCurve: 'X25519' }, false, new Uint8Array(32), 'public', ['deriveBits', 'deriveKey']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(InvalidAccessError, 'does not match'); - }); + const { x, ...otherPartyPublicKeyMissingX } = otherPartyPublicKey as JwkParamsEcPublic; + expect(() => alg.checkDeriveBitsOptions({ + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKeyMissingX }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, 'Requested operation is only valid for public keys'); + }); - it('throws an error if a private key is specified as the publicKey', () => { - const ecdhPrivateKey = new CryptoKey({ name: 'ECDH', namedCurve: 'X25519' }, false, new Uint8Array(32), 'private', ['deriveBits', 'deriveKey']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: ecdhPrivateKey }, - baseKey : ownPrivateKey - })).to.throw(InvalidAccessError, 'Requested operation is not valid'); - }); + it('throws an error if the key type of the publicKey is not EC or OKP', () => { + otherPartyPublicKey.kty = 'RSA'; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, 'Key type of the provided key must be'); + }); - it('throws an error if the baseKey property is missing', () => { - // @ts-expect-error because `baseKey` property is intentionally omitted. - expect(() => alg.checkAlgorithmOptions({ - algorithm: { name: 'ECDH', publicKey: otherPartyPublicKey } - })).to.throw(TypeError, `Required parameter missing: 'baseKey'`); - }); + it(`does not throw if publicKey 'key_ops' is undefined`, async () => { + delete otherPartyPublicKey.key_ops; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.not.throw(); + }); + + it('throws an error if a private key is specified as the publicKey', () => { + expect(() => alg.checkDeriveBitsOptions({ + // @ts-expect-error since a private key is being intentionally provided to trigger the error. + algorithm : { name: 'ECDH', publicKey: ownPrivateKey }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, 'Requested operation is only valid'); + }); + + it('throws an error if the baseKey property is missing', () => { + // @ts-expect-error because `baseKey` property is intentionally omitted. + expect(() => alg.checkDeriveBitsOptions({ + algorithm: { name: 'ECDH', publicKey: otherPartyPublicKey } + })).to.throw(TypeError, `Required parameter missing: 'baseKey'`); + }); - it('throws an error if the given baseKey is not valid', () => { + it('throws an error if the given baseKey is not valid', () => { + const { kty, ...ownPrivateKeyMissingKeyType } = ownPrivateKey as JwkParamsEcPrivate; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. - delete ownPrivateKey.extractable; - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(TypeError, 'Object is not a CryptoKey'); - }); + baseKey : ownPrivateKeyMissingKeyType + })).to.throw(TypeError, 'Object is not a JSON Web Key'); - it('throws an error if the algorithm of the baseKey does not match', () => { - const ownPrivateKey = new CryptoKey({ name: 'non-existent-algorithm', namedCurve: 'X25519' }, false, new Uint8Array(32), 'private', ['deriveBits', 'deriveKey']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(InvalidAccessError, 'does not match'); - }); + const { crv, ...ownPrivateKeyMissingCurve } = ownPrivateKey as JwkParamsEcPrivate; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + baseKey : ownPrivateKeyMissingCurve + })).to.throw(InvalidAccessError, 'Requested operation is only valid for private keys'); - it('throws an error if a public key is specified as the baseKey', () => { - const ownPrivateKey = new CryptoKey({ name: 'ECDH', namedCurve: 'X25519' }, false, new Uint8Array(32), 'public', ['deriveBits', 'deriveKey']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(InvalidAccessError, 'Requested operation is not valid'); - }); + const { x, ...ownPrivateKeyMissingX } = ownPrivateKey as JwkParamsEcPrivate; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + baseKey : ownPrivateKeyMissingX + })).to.throw(InvalidAccessError, 'Requested operation is only valid for private keys'); - it('throws an error if the named curve of the public and base keys does not match', () => { - const ownPrivateKey = new CryptoKey({ name: 'ECDH', namedCurve: 'secp256k1' }, false, new Uint8Array(32), 'private', ['deriveBits', 'deriveKey']); - expect(() => alg.checkAlgorithmOptions({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey - })).to.throw(InvalidAccessError, `named curve of the publicKey and baseKey must match`); - }); + const { d, ...ownPrivateKeyMissingD } = ownPrivateKey as JwkParamsEcPrivate; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. + baseKey : ownPrivateKeyMissingD + })).to.throw(InvalidAccessError, 'Requested operation is only valid for private keys'); }); - describe('sign()', () => { - it(`throws an error because 'sign' operation is valid for ECDH keys`, async () => { - await expect(alg.sign()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for ECDH'); - }); + it('throws an error if the key type of the baseKey is not EC or OKP', () => { + ownPrivateKey.kty = 'RSA'; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, 'Key type of the provided key must be'); + }); + + it(`does not throw if baseKey 'key_ops' is undefined`, async () => { + delete ownPrivateKey.key_ops; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.not.throw(); }); - describe('verify()', () => { - it(`throws an error because 'verify' operation is valid for ECDH keys`, async () => { - await expect(alg.verify()).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for ECDH'); + it('throws an error if a public key is specified as the baseKey', () => { + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + // @ts-expect-error because public key is being provided instead of private key. + baseKey : otherPartyPublicKey + })).to.throw(InvalidAccessError, 'Requested operation is only valid for private keys'); + }); + + it('throws an error if the key type of the public and base keys does not match', () => { + ownPrivateKey.kty = 'EC'; + otherPartyPublicKey.kty = 'OKP'; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, `key type of the publicKey and baseKey must match`); + }); + + it('throws an error if the curve of the public and base keys does not match', () => { + (ownPrivateKey as JwkParamsEcPrivate).crv = 'secp256k1'; + (otherPartyPublicKey as JwkParamsOkpPublic).crv = 'X25519'; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, `curve of the publicKey and baseKey must match`); + }); + + ['baseKey', 'publicKey'].forEach(keyType => { + describe(`if ${keyType} 'key_ops' is specified`, () => { + it(`does not throw if 'key_ops' is valid`, () => { + const key = keyType === 'baseKey' ? ownPrivateKey : otherPartyPublicKey; + key.key_ops = ['deriveBits']; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.not.throw(); + }); + + it(`throws an error if 'key_ops' property is an empty array`, () => { + const key = keyType === 'baseKey' ? ownPrivateKey : otherPartyPublicKey; + key.key_ops = []; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(InvalidAccessError, `is not valid for the provided key`); + }); + + it(`throws an error if the 'key_ops' property is not an array`, () => { + const key = keyType === 'baseKey' ? ownPrivateKey : otherPartyPublicKey; + key.key_ops = 'deriveBits' as any; // Intentionally incorrect type + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey + })).to.throw(TypeError, `is not of type Array.`); + }); + + it(`throws an error if the 'key_ops' property contains an invalid operation`, () => { + const key = keyType === 'baseKey' ? ownPrivateKey : otherPartyPublicKey; + key.key_ops = ['sign']; + expect(() => alg.checkDeriveBitsOptions({ + algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, + baseKey : ownPrivateKey, + })).to.throw(InvalidAccessError, `is not valid for the provided key`); + }); }); }); }); - describe('BaseEcdsaAlgorithm', () => { - let alg: BaseEcdsaAlgorithm; + describe('sign()', () => { + it(`throws an error because 'sign' operation is not valid for ECDH`, async () => { + await expect(alg.sign()).to.eventually.be.rejectedWith(InvalidAccessError, `is not valid for ECDH`); + }); + }); - before(() => { - alg = Reflect.construct(BaseEcdsaAlgorithm, []) as BaseEcdsaAlgorithm; - // @ts-expect-error because `hashAlgorithms` is a read-only property. - alg.hashAlgorithms = ['SHA-256']; + describe('verify()', () => { + it(`throws an error because 'verify' operation is not valid for ECDH`, async () => { + await expect(alg.verify()).to.eventually.be.rejectedWith(InvalidAccessError, `is not valid for ECDH`); }); + }); + }); - describe('checkAlgorithmOptions()', () => { - it('does not throw with matching algorithm name and valid hash algorithm', () => { - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'ECDSA', - hash : 'SHA-256' - }})).to.not.throw(); - }); + describe('BaseEcdsaAlgorithm', () => { + let alg: BaseEcdsaAlgorithm; - it('throws an error when unsupported algorithm specified', () => { - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'Nope', - hash : 'SHA-256' - }})).to.throw(NotSupportedError, 'Algorithm not supported'); - }); + before(() => { + alg = Reflect.construct(BaseEcdsaAlgorithm, []) as BaseEcdsaAlgorithm; + // @ts-expect-error because the `names` property is readonly. + alg.names = ['ES256K'] as const; + // @ts-expect-error because the `curves` property is readonly. + alg.curves = ['secp256k1'] as const; + }); - it('throws an error if the hash property is missing', () => { - // @ts-expect-error because `hash` property is intentionally omitted. - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name: 'ECDSA', - }})).to.throw(TypeError, 'Required parameter missing'); - }); + describe('checkSignOptions()', () => { + it('validates that key is an EC private key', async () => { + const ed25519PrivateKey: PrivateKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : 'k-DgyL6dBSdblokVYrYfJhSAEbf3gx68YSTwtqAaMis', + d : 'VF2v7AbPoDwuuTcV-M6mB_C7SYIDB4E0ImvGM3t0VAE' + }; - it('throws an error if the given hash algorithm is not supported', () => { - const ecdhPublicKey = new CryptoKey({ name: 'ECDH', namedCurve: 'X25519' }, false, new Uint8Array(32), 'public', ['deriveBits', 'deriveKey']); - // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. - delete ecdhPublicKey.extractable; - expect(() => alg.checkAlgorithmOptions({ algorithm: { - name : 'ECDSA', - hash : 'SHA-1234' - }})).to.throw(TypeError, 'Out of range'); - }); + expect(() => alg.checkSignOptions({ + algorithm : { name: 'ES256K' }, + key : ed25519PrivateKey, + data : new Uint8Array([51, 52, 53]) + })).to.throw(InvalidAccessError, 'operation is only valid for EC private keys'); }); + }); - describe('deriveBits()', () => { - it(`throws an error because 'deriveBits' operation is valid for ECDSA keys`, async () => { - await expect(alg.deriveBits()).to.eventually.be.rejectedWith(InvalidAccessError, `is not valid for ECDSA`); - }); + describe('checkVerifyOptions()', () => { + it('validates that key is an EC public key', async () => { + const ed25519PublicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : 'k-DgyL6dBSdblokVYrYfJhSAEbf3gx68YSTwtqAaMis', + }; + + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'ES256K' }, + key : ed25519PublicKey, + data : new Uint8Array(), + signature : new Uint8Array() + })).to.throw(InvalidAccessError, 'operation is only valid for EC public keys'); }); }); - describe('BaseEdDsaAlgorithm', () => { - let alg: BaseEdDsaAlgorithm; - - before(() => { - alg = Reflect.construct(BaseEdDsaAlgorithm, []) as BaseEdDsaAlgorithm; + describe('deriveBits()', () => { + it(`throws an error because 'deriveBits' operation is not valid for ECDSA algorithm`, async () => { + await expect(alg.deriveBits()).to.eventually.be.rejectedWith(InvalidAccessError, `is not valid for ECDSA`); }); + }); + }); - describe('checkAlgorithmOptions()', () => { - const testEdDsaAlgorithm = Reflect.construct(BaseEdDsaAlgorithm, []) as BaseEdDsaAlgorithm; + describe('BaseEdDsaAlgorithm', () => { + let alg: BaseEdDsaAlgorithm; - it('does not throw with matching algorithm name', () => { - expect(() => testEdDsaAlgorithm.checkAlgorithmOptions({ algorithm: { - name: 'EdDSA' - }})).to.not.throw(); - }); + before(() => { + alg = Reflect.construct(BaseEdDsaAlgorithm, []) as BaseEdDsaAlgorithm; + // @ts-expect-error because the `names` property is readonly. + alg.names = ['EdDSA'] as const; + // @ts-expect-error because the `curves` property is readonly. + alg.curves = ['Ed25519'] as const; + }); - it('throws an error when unsupported algorithm specified', () => { - expect(() => testEdDsaAlgorithm.checkAlgorithmOptions({ algorithm: { - name: 'Nope' - }})).to.throw(NotSupportedError, 'Algorithm not supported'); - }); + describe('checkSignOptions()', () => { + it('validates that key is an OKP private key', async () => { + const secp256k1PrivateKey: PrivateKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : 'UxYbeCQo17viyn9Bb5frn80_icQ0dHaRNsjfjZDaxDo', + y : '5vg_APq25qhV1wkbEqT3Z1H8vt57iHDhQqsw9TN0M1E', + d : 'O2-jjd6m16BXjxTp-UudzZNIkRHQwUYN0KJg3i5Ndko' + }; + + expect(() => alg.checkSignOptions({ + algorithm : { name: 'EdDSA' }, + key : secp256k1PrivateKey, + data : new Uint8Array([51, 52, 53]) + })).to.throw(InvalidAccessError, 'operation is only valid for OKP private keys'); }); + }); - describe('deriveBits()', () => { - it(`throws an error because 'deriveBits' operation is valid for EdDSA keys`, async () => { - await expect(alg.deriveBits()).to.eventually.be.rejectedWith(InvalidAccessError, `is not valid for EdDSA`); - }); + describe('checkVerifyOptions()', () => { + it('validates that key is an OKP public key', async () => { + const secp256k1PublicKey: PublicKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : 'UxYbeCQo17viyn9Bb5frn80_icQ0dHaRNsjfjZDaxDo', + y : '5vg_APq25qhV1wkbEqT3Z1H8vt57iHDhQqsw9TN0M1E' + }; + + expect(() => alg.checkVerifyOptions({ + algorithm : { name: 'EdDSA' }, + key : secp256k1PublicKey, + data : new Uint8Array(), + signature : new Uint8Array() + })).to.throw(InvalidAccessError, 'operation is only valid for OKP public keys'); + }); + }); + + describe('deriveBits()', () => { + it(`throws an error because 'deriveBits' operation is not valid for EdDSA keys`, async () => { + await expect(alg.deriveBits()).to.eventually.be.rejectedWith(InvalidAccessError, `is not valid for EdDSA`); }); }); }); @@ -688,16 +1082,22 @@ describe('Algorithms API', () => { before(() => { alg = Reflect.construct(BasePbkdf2Algorithm, []) as BasePbkdf2Algorithm; + // @ts-expect-error because the `names` property is readonly. + alg.names = ['PBKDF2' as const]; // @ts-expect-error because `hashAlgorithms` is a read-only property. alg.hashAlgorithms = ['SHA-256']; }); describe('checkAlgorithmOptions()', () => { - let baseKey: Web5Crypto.CryptoKey; + let baseKey: PrivateKeyJwk; beforeEach(() => { - baseKey = new CryptoKey({ name: 'PBKDF2' }, false, new Uint8Array(32), 'secret', ['deriveBits', 'deriveKey']); + baseKey = { + kty : 'oct', + k : Convert.uint8Array(new Uint8Array(32)).toBase64Url(), + key_ops : ['deriveBits', 'deriveKey'] + }; }); it('does not throw with matching algorithm name and valid hash, iterations, and salt', () => { @@ -824,7 +1224,7 @@ describe('Algorithms API', () => { it('throws an error if the given key is not valid', () => { // @ts-ignore-error because a required property is being intentionally deleted to trigger the check to throw. - delete baseKey.extractable; + delete baseKey.kty; expect(() => alg.checkAlgorithmOptions({ algorithm: { name : 'PBKDF2', @@ -833,11 +1233,15 @@ describe('Algorithms API', () => { salt : new Uint8Array(16) }, baseKey - })).to.throw(TypeError, 'Object is not a CryptoKey'); + })).to.throw(TypeError, 'Object is not a JSON Web Key'); }); - it('throws an error if the algorithm of the key does not match', () => { - const baseKey = new CryptoKey({ name: 'wrong-algorithm' }, false, new Uint8Array(32), 'secret', ['deriveBits', 'deriveKey']); + it('throws an error if the key type of the key is not valid', () => { + const baseKey: PrivateKeyJwk = { + kty : 'OKP', + // @ts-expect-error because OKP JWKs don't have a k parameter. + k : Convert.uint8Array(new Uint8Array(32)).toBase64Url() + }; expect(() => alg.checkAlgorithmOptions({ algorithm: { name : 'PBKDF2', @@ -846,56 +1250,7 @@ describe('Algorithms API', () => { salt : new Uint8Array(16) }, baseKey - })).to.throw(InvalidAccessError, 'does not match'); - }); - }); - - describe('checkImportKey()', () => { - it('should not throw when all options are valid', () => { - expect(() => alg.checkImportKey({ - algorithm : { name: 'PBKDF2' }, - format : 'raw', - extractable : false, - keyUsages : ['deriveBits'] - })).to.not.throw(); - }); - - it('throws an error when unsupported algorithm specified', () => { - expect(() => alg.checkImportKey({ - algorithm : { name: 'ECDH' }, - format : 'raw', - extractable : false, - keyUsages : ['deriveBits'] - })).to.throw(NotSupportedError, 'Algorithm not supported'); - }); - - it('throws an error if the format is not raw', () => { - expect(() => alg.checkImportKey({ - algorithm : { name: 'PBKDF2' }, - format : 'pkcs8', // Invalid, only 'raw' is supported - extractable : false, - keyUsages : ['deriveBits'] - })).to.throw(SyntaxError, `Only 'raw' is supported`); - }); - - it('throws an error if extractable is not false', () => { - expect(() => alg.checkImportKey({ - algorithm : { name: 'PBKDF2' }, - format : 'raw', - extractable : true, - keyUsages : ['deriveBits'] - })).to.throw(SyntaxError, `Only 'false' is supported`); - }); - - it('throws an error when the requested operation is not valid', () => { - ['sign', 'verify'].forEach((operation) => { - expect(() => alg.checkImportKey({ - algorithm : { name: 'PBKDF2' }, - format : 'raw', - extractable : false, - keyUsages : [operation as KeyUsage] - })).to.throw(InvalidAccessError, 'Requested operation'); - }); + })).to.throw(InvalidAccessError, 'Key type of the provided key must be'); }); }); diff --git a/packages/crypto/tests/crypto-algorithms.spec.ts b/packages/crypto/tests/crypto-algorithms.spec.ts index d62c8b200..1c815dc8a 100644 --- a/packages/crypto/tests/crypto-algorithms.spec.ts +++ b/packages/crypto/tests/crypto-algorithms.spec.ts @@ -1,13 +1,13 @@ -import type { Web5Crypto } from '../src/types/web5-crypto.js'; - import sinon from 'sinon'; import chai, { expect } from 'chai'; import { Convert } from '@web5/common'; import chaiAsPromised from 'chai-as-promised'; +import type { JwkParamsOctPrivate, PrivateKeyJwk, PublicKeyJwk } from '../src/jose.js'; + import { aesCtrTestVectors } from './fixtures/test-vectors/aes.js'; import { AesCtr, Ed25519, Secp256k1, X25519 } from '../src/crypto-primitives/index.js'; -import { CryptoKey, InvalidAccessError, NotSupportedError, OperationError } from '../src/algorithms-api/index.js'; +import { InvalidAccessError, NotSupportedError, OperationError } from '../src/algorithms-api/index.js'; import { EcdhAlgorithm, EcdsaAlgorithm, @@ -28,24 +28,23 @@ describe('Default Crypto Algorithm Implementations', () => { }); describe('decrypt()', () => { - let secretCryptoKey: Web5Crypto.CryptoKey; + let dataEncryptionKey: PrivateKeyJwk; - beforeEach(async () => { - secretCryptoKey = await aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : false, - keyUsages : ['encrypt', 'decrypt'] + before(async () => { + dataEncryptionKey = await aesCtr.generateKey({ + algorithm : { name: 'A128CTR' }, + keyOperations : ['encrypt', 'decrypt'] }); }); it('returns plaintext as a Uint8Array', async () => { const plaintext = await aesCtr.decrypt({ algorithm: { - name : 'AES-CTR', + name : 'A128CTR', counter : new Uint8Array(16), length : 128 }, - key : secretCryptoKey, + key : dataEncryptionKey, data : new Uint8Array([1, 2, 3, 4]) }); @@ -53,247 +52,121 @@ describe('Default Crypto Algorithm Implementations', () => { expect(plaintext.byteLength).to.equal(4); }); - it('returns plaintext given ciphertext', async () => { - let secretCryptoKey: Web5Crypto.CryptoKey; + it('returns expected plaintext given ciphertext input', async () => { + let dataEncryptionKey: PrivateKeyJwk; for (const vector of aesCtrTestVectors) { - secretCryptoKey = new CryptoKey( - { name: 'AES-CTR', length: 128 }, - false, - Convert.hex(vector.key).toUint8Array(), - 'secret', - ['encrypt', 'decrypt'] - ); + dataEncryptionKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes: Convert.hex(vector.key).toUint8Array() }); + const plaintext = await aesCtr.decrypt({ algorithm: { - name : 'AES-CTR', + name : 'A128CTR', counter : Convert.hex(vector.counter).toUint8Array(), length : vector.length }, - key : secretCryptoKey, + key : dataEncryptionKey, data : Convert.hex(vector.ciphertext).toUint8Array() }); expect(Convert.uint8Array(plaintext).toHex()).to.deep.equal(vector.data); } }); - it('validates algorithm, counter, and length', async () => { - const secretCryptoKey: Web5Crypto.CryptoKey = new CryptoKey( - { name: 'AES-CTR', length: 128 }, - false, - new Uint8Array(16), - 'secret', - ['encrypt', 'decrypt'] - ); - - // Invalid (algorithm name, counter, length) result in algorithm name check failing first. - await expect(aesCtr.decrypt({ - algorithm : { name: 'foo', counter: new Uint8Array(64), length: 512 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - - // Valid (algorithm name) + Invalid (counter, length) result counter check failing first. - await expect(aesCtr.decrypt({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(64), length: 512 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(OperationError, `'counter' must have length`); - - // Valid (algorithm name, counter) + Invalid (length) result length check failing first. - await expect(aesCtr.decrypt({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 512 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(OperationError, `'length' should be in the range`); - }); - - it(`validates that key usage is 'decrypt'`, async () => { - // Manually specify the secret key usages to exclude the 'decrypt' operation. - secretCryptoKey.usages = ['encrypt']; - - await expect(aesCtr.decrypt({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 128 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); - }); - }); - - describe('encrypt()', () => { - let secretCryptoKey: Web5Crypto.CryptoKey; + describe('encrypt()', () => { + let dataEncryptionKey: PrivateKeyJwk; - before(async () => { - secretCryptoKey = await aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : false, - keyUsages : ['encrypt', 'decrypt'] - }); - }); - - it('returns ciphertext as a Uint8Array', async () => { - const ciphertext = await aesCtr.encrypt({ - algorithm: { - name : 'AES-CTR', - counter : new Uint8Array(16), - length : 128 - }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) + before(async () => { + dataEncryptionKey = await aesCtr.generateKey({ + algorithm : { name: 'A128CTR' }, + keyOperations : ['encrypt', 'decrypt'] + }); }); - expect(ciphertext).to.be.instanceOf(Uint8Array); - expect(ciphertext.byteLength).to.equal(4); - }); - - it('returns ciphertext given plaintext', async () => { - let secretCryptoKey: Web5Crypto.CryptoKey; - for (const vector of aesCtrTestVectors) { - secretCryptoKey = new CryptoKey( - { name: 'AES-CTR', length: 128 }, - false, - Convert.hex(vector.key).toUint8Array(), - 'secret', - ['encrypt', 'decrypt'] - ); + it('returns ciphertext as a Uint8Array', async () => { const ciphertext = await aesCtr.encrypt({ algorithm: { - name : 'AES-CTR', - counter : Convert.hex(vector.counter).toUint8Array(), - length : vector.length + name : 'A128CTR', + counter : new Uint8Array(16), + length : 128 }, - key : secretCryptoKey, - data : Convert.hex(vector.data).toUint8Array() + key : dataEncryptionKey, + data : new Uint8Array([1, 2, 3, 4]) }); - expect(Convert.uint8Array(ciphertext).toHex()).to.deep.equal(vector.ciphertext); - } - }); - it('validates algorithm, counter, and length', async () => { - const secretCryptoKey: Web5Crypto.CryptoKey = new CryptoKey( - { name: 'AES-CTR', length: 128 }, - false, - new Uint8Array(16), - 'secret', - ['encrypt', 'decrypt'] - ); - - // Invalid (algorithm name, counter, length) result in algorithm name check failing first. - await expect(aesCtr.encrypt({ - algorithm : { name: 'foo', counter: new Uint8Array(64), length: 512 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); + expect(ciphertext).to.be.instanceOf(Uint8Array); + expect(ciphertext.byteLength).to.equal(4); + }); - // Valid (algorithm name) + Invalid (counter, length) result counter check failing first. - await expect(aesCtr.encrypt({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(64), length: 512 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(OperationError, `'counter' must have length`); - - // Valid (algorithm name, counter) + Invalid (length) result length check failing first. - await expect(aesCtr.encrypt({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 512 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(OperationError, `'length' should be in the range`); - }); - - it(`validates that key usage is 'encrypt'`, async () => { - // Manually specify the secret key usages to exclude the 'encrypt' operation. - secretCryptoKey.usages = ['decrypt']; - - await expect(aesCtr.encrypt({ - algorithm : { name: 'AES-CTR', counter: new Uint8Array(16), length: 128 }, - key : secretCryptoKey, - data : new Uint8Array([1, 2, 3, 4]) - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); + it('returns expected ciphertext given plaintext input', async () => { + for (const vector of aesCtrTestVectors) { + dataEncryptionKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes: Convert.hex(vector.key).toUint8Array() }); + + const ciphertext = await aesCtr.encrypt({ + algorithm: { + name : 'A128CTR', + counter : Convert.hex(vector.counter).toUint8Array(), + length : vector.length + }, + key : dataEncryptionKey, + data : Convert.hex(vector.data).toUint8Array() + }); + expect(Convert.uint8Array(ciphertext).toHex()).to.deep.equal(vector.ciphertext); + } + }); }); }); describe('generateKey()', () => { - it('returns a secret key', async () => { - const key = await aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : false, - keyUsages : ['encrypt', 'decrypt'] + it('returns a private key in JWK format', async () => { + const privateKey = await aesCtr.generateKey({ + algorithm : { name: 'A128CTR' }, + keyOperations : ['encrypt', 'decrypt'] }); - expect(key.algorithm.name).to.equal('AES-CTR'); - expect(key.usages).to.deep.equal(['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); - expect(key.material.byteLength).to.equal(128 / 8); - }); - - it('secret key is selectively extractable', async () => { - let key: CryptoKey; - // key is NOT extractable if generateKey() called with extractable = false - key = await aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : false, - keyUsages : ['encrypt', 'decrypt'] - }); - expect(key.extractable).to.be.false; + expect(privateKey).to.have.property('alg', 'A128CTR'); + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); - // key is extractable if generateKey() called with extractable = true - key = await aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : true, - keyUsages : ['encrypt', 'decrypt'] - }); - expect(key.extractable).to.be.true; + expect(privateKey.key_ops).to.deep.equal(['encrypt', 'decrypt']); }); - it(`supports 'encrypt', 'decrypt', 'wrapKey', and/or 'unWrapKey' key usages`, async () => { - const operations = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - for (const operation of operations) { + it(`supports 'A128CTR', 'A192CTR', and 'A256CTR' algorithms`, async () => { + const algorithms = ['A128CTR', 'A192CTR', 'A256CTR']; + for (const algorithm of algorithms) { await expect(aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : true, - keyUsages : [operation as KeyUsage] + algorithm : { name: algorithm }, + keyOperations : ['encrypt', 'decrypt'] })).to.eventually.be.fulfilled; } }); - it('validates algorithm, length, and key usages', async () => { - // Invalid (algorithm name, length, and key usages) result in algorithm name check failing first. - await expect(aesCtr.generateKey({ - algorithm : { name: 'foo', length: 512 }, - extractable : false, - keyUsages : ['sign'] - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - - // Valid (algorithm name) + Invalid (length, key usages) result length check failing first. - await expect(aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 512 }, - extractable : false, - keyUsages : ['sign'] - })).to.eventually.be.rejectedWith(OperationError, `'length' must be 128, 192, or 256`); - - // Valid (algorithm name, length) + Invalid (key usages) result key usages check failing first. - await expect(aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 256 }, - extractable : false, - keyUsages : ['sign'] - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation'); + it(`returns keys with the correct bit length`, async () => { + const algorithms = ['A128CTR', 'A192CTR', 'A256CTR']; + for (const algorithm of algorithms) { + const privateKey = await aesCtr.generateKey({ + algorithm : { name: algorithm }, + keyOperations : ['encrypt', 'decrypt'] + }) as JwkParamsOctPrivate; + const privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength * 8).to.equal(parseInt(algorithm.slice(1, 4))); + } }); - it(`should throw an error if 'AES-CTR' key generation fails`, async function() { - // @ts-ignore because the method is being intentionally stubbed to return null. - const aesCtrStub = sinon.stub(AesCtr, 'generateKey').returns(Promise.resolve(null)); + it(`throws an error if operation fails`, async function() { + const checkGenerateKeyOptionsStub = sinon.stub(aesCtr, 'checkGenerateKeyOptions').returns(undefined); + // @ts-expect-error because method is being intentionally stubbed to return undefined. + const checkGenerateKeyStub = sinon.stub(AesCtr, 'generateKey').returns(Promise.resolve(undefined)); try { - await aesCtr.generateKey({ - algorithm : { name: 'AES-CTR', length: 128 }, - extractable : false, - keyUsages : ['encrypt', 'decrypt'] - }); - aesCtrStub.restore(); - expect.fail('Expect generateKey() to throw an error'); + // @ts-expect-error because no generateKey operations are defined. + await aesCtr.generateKey({ algorithm: {} }); + expect.fail('Expected aesCtr.generateKey() to throw an error'); } catch (error) { - aesCtrStub.restore(); expect(error).to.be.an('error'); - expect((error as Error).message).to.equal('Operation failed to generate key.'); + expect((error as Error).message).to.include('Operation failed: generateKey'); + } finally { + checkGenerateKeyOptionsStub.restore(); + checkGenerateKeyStub.restore(); } }); }); @@ -308,360 +181,286 @@ describe('Default Crypto Algorithm Implementations', () => { describe('deriveBits()', () => { - let otherPartyPublicKey: Web5Crypto.CryptoKey; - let ownPrivateKey: Web5Crypto.CryptoKey; + let secp256k1PrivateKeyA: PrivateKeyJwk; + let secp256k1PublicKeyA: PublicKeyJwk; + let secp256k1PrivateKeyB: PrivateKeyJwk; + let secp256k1PublicKeyB: PublicKeyJwk; - beforeEach(async () => { - const otherPartyKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - otherPartyPublicKey = otherPartyKeyPair.publicKey; + let x25519PrivateKeyA: PrivateKeyJwk; + let x25519PublicKeyA: PublicKeyJwk; + let x25519PrivateKeyB: PrivateKeyJwk; + let x25519PublicKeyB: PublicKeyJwk; - const ownKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - ownPrivateKey = ownKeyPair.privateKey; + before(async () => { + secp256k1PrivateKeyA = await ecdh.generateKey({ algorithm: { name: 'ECDH', curve: 'secp256k1' } }); + secp256k1PublicKeyA = await Secp256k1.computePublicKey({ privateKey: secp256k1PrivateKeyA }); + secp256k1PrivateKeyB = await ecdh.generateKey({ algorithm: { name: 'ECDH', curve: 'secp256k1' } }); + secp256k1PublicKeyB = await Secp256k1.computePublicKey({ privateKey: secp256k1PrivateKeyB }); + + x25519PrivateKeyA = await ecdh.generateKey({ algorithm: { name: 'ECDH', curve: 'X25519' } }); + x25519PublicKeyA = await X25519.computePublicKey({ privateKey: x25519PrivateKeyA }); + x25519PrivateKeyB = await ecdh.generateKey({ algorithm: { name: 'ECDH', curve: 'X25519' } }); + x25519PublicKeyB = await X25519.computePublicKey({ privateKey: x25519PrivateKeyB }); }); - it('returns shared secrets with maximum bit length when length is null', async () => { - const sharedSecretSecp256k1 = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : null + it(`supports 'secp256k1' curve`, async () => { + const sharedSecret = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey : secp256k1PrivateKeyA }); + expect(sharedSecret).to.be.instanceOf(Uint8Array); + expect(sharedSecret.byteLength).to.equal(32); + }); - const otherPartyKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] + it(`supports 'X25519' curve`, async () => { + const sharedSecret = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: x25519PublicKeyB }, + baseKey : x25519PrivateKeyA }); - otherPartyPublicKey = otherPartyKeyPair.publicKey; + expect(sharedSecret).to.be.instanceOf(Uint8Array); + expect(sharedSecret.byteLength).to.equal(32); + }); - const ownKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - ownPrivateKey = ownKeyPair.privateKey; + it('throws an error when base or public key is an unsupported curve', async () => { + // Manually change the key's curve to trigger an error. + // @ts-expect-error because an unknown 'crv' value is manually set. + const baseKey = { ...secp256k1PrivateKeyA, crv: 'non-existent-curve' } as PrivateKeyJwk; + // @ts-expect-error because an unknown 'crv' value is manually set. + const publicKey = { ...secp256k1PublicKeyB, crv: 'non-existent-curve' } as PublicKeyJwk; - const sharedSecretX25519 = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : null - }); - expect(sharedSecretSecp256k1.byteLength).to.equal(32); - expect(sharedSecretX25519.byteLength).to.equal(32); + await expect(ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey }, + baseKey, + length : 40 + })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); }); - it('returns shared secrets with specified length, if possible', async () => { - let sharedSecretSecp256k1 = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : 16 + it('returns shared secret with maximum bit length when length is null', async () => { + const sharedSecretSecp256k1 = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey : secp256k1PrivateKeyA }); - expect(sharedSecretSecp256k1.byteLength).to.equal(16 / 8); + expect(sharedSecretSecp256k1.byteLength).to.equal(32); + }); - sharedSecretSecp256k1 = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : 256 + it('returns shared secret with specified length, if possible', async () => { + [16, 32, 256].forEach(async length => { + let sharedSecretSecp256k1 = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey : secp256k1PrivateKeyA, + length + }); + expect(sharedSecretSecp256k1.byteLength).to.equal(length / 8); }); - expect(sharedSecretSecp256k1.byteLength).to.equal(256 / 8); + }); - const otherPartyKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] + it('is commutative', async () => { + const sharedSecretSecp256k1 = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey : secp256k1PrivateKeyA }); - otherPartyPublicKey = otherPartyKeyPair.publicKey; - - const ownKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] + const sharedSecretSecp256k1Reversed = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyA }, + baseKey : secp256k1PrivateKeyB }); - ownPrivateKey = ownKeyPair.privateKey; + expect(sharedSecretSecp256k1).to.deep.equal(sharedSecretSecp256k1Reversed); const sharedSecretX25519 = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : 32 + algorithm : { name: 'ECDH', publicKey: x25519PublicKeyB }, + baseKey : x25519PrivateKeyA + }); + const sharedSecretX25519Reversed = await ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: x25519PublicKeyA }, + baseKey : x25519PrivateKeyB }); - expect(sharedSecretX25519.byteLength).to.equal(32 / 8); + expect(sharedSecretX25519).to.deep.equal(sharedSecretX25519Reversed); }); it('throws error if requested length exceeds that of the generated shared secret', async () => { - await expect(ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : 264 - })).to.eventually.be.rejectedWith(OperationError, `Requested 'length' exceeds the byte length of the derived secret`); + await expect( + ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey : secp256k1PrivateKeyA, + length : 264 + }) + ).to.eventually.be.rejectedWith(OperationError, `Requested 'length' exceeds the byte length of the derived secret`); }); it('throws an error if the given length is not a multiple of 8', async () => { await expect(ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey : secp256k1PrivateKeyA, length : 127 })).to.eventually.be.rejectedWith(OperationError, `'length' must be a multiple of 8`); }); - it(`supports 'secp256k1' curve`, async () => { - const sharedSecret = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : null - }); - expect(sharedSecret).to.be.instanceOf(Uint8Array); - expect(sharedSecret.byteLength).to.equal(32); - }); - - it(`supports 'X25519' curve`, async () => { - const otherPartyKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - otherPartyPublicKey = otherPartyKeyPair.publicKey; - - const ownKeyPair = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - ownPrivateKey = ownKeyPair.privateKey; - - const sharedSecret = await ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : null - }); - - expect(sharedSecret).to.be.instanceOf(Uint8Array); - expect(sharedSecret.byteLength).to.equal(32); - }); - - it(`validates that key usage is 'deriveBits'`, async () => { - // Manually specify the private key usages to exclude the 'deriveBits' operation. - otherPartyPublicKey.usages = ['sign']; + it(`accepts base key without 'key_ops' set`, async () => { + const baseKey = { ...secp256k1PrivateKeyA, key_ops: undefined } as PrivateKeyJwk; await expect(ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : null - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); + algorithm: { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey + })).to.eventually.be.fulfilled; }); - it('throws an error when key(s) is an unsupported curve', async () => { - // Manually change the key's named curve to trigger an error. - // @ts-expect-error because TS can't determine the type of key. - otherPartyPublicKey.algorithm.namedCurve = 'non-existent-curve'; - // @ts-expect-error because TS can't determine the type of key. - ownPrivateKey.algorithm.namedCurve = 'non-existent-curve'; + it(`accepts public key without 'key_ops' set`, async () => { + const publicKey = { ...secp256k1PublicKeyB, key_ops: undefined } as PublicKeyJwk; await expect(ecdh.deriveBits({ - algorithm : { name: 'ECDH', publicKey: otherPartyPublicKey }, - baseKey : ownPrivateKey, - length : 40 - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + algorithm : { name: 'ECDH', publicKey }, + baseKey : secp256k1PrivateKeyA + })).to.eventually.be.fulfilled; }); - }); - describe('generateKey()', () => { - it('returns a key pair', async () => { - const keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] - }); + it(`if specified, validates base key operations includes 'deriveBits'`, async () => { + // Manually specify the base key operations array to exclude the 'deriveBits' operation. + const baseKey = { ...secp256k1PrivateKeyA, key_ops: ['sign'] } as PrivateKeyJwk; - expect(keys).to.have.property('privateKey'); - expect(keys.privateKey.type).to.equal('private'); - expect(keys.privateKey.usages).to.deep.equal(['deriveBits', 'deriveKey']); - - expect(keys).to.have.property('publicKey'); - expect(keys.publicKey.type).to.equal('public'); - expect(keys.publicKey.usages).to.deep.equal(['deriveBits', 'deriveKey']); + await expect(ecdh.deriveBits({ + algorithm: { name: 'ECDH', publicKey: secp256k1PublicKeyB }, + baseKey + })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); }); - it('public key is always extractable', async () => { - let keys: CryptoKeyPair; - // publicKey is extractable if generateKey() called with extractable = false - keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] - }); - expect(keys.publicKey.extractable).to.be.true; + it(`if specified, validates public key operations includes 'deriveBits'`, async () => { + // Manually specify the private key operations array to exclude the 'deriveBits' operation. + const publicKey = { ...secp256k1PublicKeyB, key_ops: ['sign'] } as PublicKeyJwk; - // publicKey is extractable if generateKey() called with extractable = true - keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : true, - keyUsages : ['deriveBits', 'deriveKey'] - }); - expect(keys.publicKey.extractable).to.be.true; + await expect( + ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey }, + baseKey : secp256k1PrivateKeyA + }) + ).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); }); - it('private key is selectively extractable', async () => { - let keys: CryptoKeyPair; - // privateKey is NOT extractable if generateKey() called with extractable = false - keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] - }); - expect(keys.privateKey.extractable).to.be.false; - - // privateKey is extractable if generateKey() called with extractable = true - keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : true, - keyUsages : ['deriveBits', 'deriveKey'] - }); - expect(keys.privateKey.extractable).to.be.true; + it('throws an error if the public/private keys from the same key pair are specified', async () => { + await expect( + ecdh.deriveBits({ + algorithm : { name: 'ECDH', publicKey: secp256k1PublicKeyA }, + baseKey : secp256k1PrivateKeyA + }) + ).to.eventually.be.rejectedWith(Error, 'shared secret cannot be computed from a single key pair'); }); + }); - it(`supports 'secp256k1' curve with compressed public keys, by default`, async () => { - const keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await ecdh.generateKey({ + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['deriveBits', 'deriveKey'] }); - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('compressedPublicKey' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.compressedPublicKey).to.be.true; - }); - - it(`supports 'secp256k1' curve with compressed public keys`, async () => { - const keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'secp256k1', compressedPublicKey: true }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] - }); + expect(privateKey).to.have.property('crv', 'X25519'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('x'); - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('compressedPublicKey' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.compressedPublicKey).to.be.true; + expect(privateKey.key_ops).to.deep.equal(['deriveBits', 'deriveKey']); }); - it(`supports 'secp256k1' curve with uncompressed public keys`, async () => { - const keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'secp256k1', compressedPublicKey: false }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] + it(`supports 'secp256k1' curve`, async () => { + const privateKey = await ecdh.generateKey({ + algorithm : { name: 'ECDH', curve: 'secp256k1' }, + keyOperations : ['deriveBits', 'deriveKey'] }); - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('compressedPublicKey' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.compressedPublicKey).to.be.false; + if (!('crv' in privateKey)) throw new Error; // TS type guard + expect(privateKey.crv).to.equal('secp256k1'); }); it(`supports 'X25519' curve`, async () => { - const keys = await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] + const privateKey = await ecdh.generateKey({ + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['deriveBits', 'deriveKey'] }); - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('X25519'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('X25519'); + if (!('crv' in privateKey)) throw new Error; // TS type guard + expect(privateKey.crv).to.equal('X25519'); }); - it(`supports 'deriveBits' and/or 'deriveKey' key usages`, async () => { + it(`supports 'deriveBits' and/or 'deriveKey' key operations`, async () => { await expect(ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits'] + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['deriveBits'] })).to.eventually.be.fulfilled; await expect(ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveKey'] + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['deriveKey'] })).to.eventually.be.fulfilled; await expect(ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['deriveBits', 'deriveKey'] })).to.eventually.be.fulfilled; }); - it('validates algorithm, named curve, and key usages', async () => { - // Invalid (algorithm name, named curve, and key usages) result in algorithm name check failing first. + it(`accepts 'keyOperations' as undefined`, async () => { + const privateKey = await ecdh.generateKey({ + algorithm: { name: 'ECDH', curve: 'X25519' }, + }); + + expect(privateKey).to.exist; + expect(privateKey.key_ops).to.be.undefined; + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('crv', 'X25519'); + }); + + it('validates algorithm, curve, and key operations', async () => { + // Invalid (algorithm name, named curve, and key operations) result in algorithm name check failing first. await expect(ecdh.generateKey({ - algorithm : { name: 'foo', namedCurve: 'bar' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm : { name: 'foo', curve: 'bar' }, + keyOperations : ['encrypt'] })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - // Valid (algorithm name) + Invalid (named curve, key usages) result named curve check failing first. + // Valid (algorithm name) + Invalid (named curve, key operations) result named curve check failing first. await expect(ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'bar' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm : { name: 'ECDH', curve: 'bar' }, + keyOperations : ['encrypt'] })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); - // Valid (algorithm name, named curve) + Invalid (key usages) result key usages check failing first. + // Valid (algorithm name, named curve) + Invalid (key operations) result key operations check failing first. await expect(ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['encrypt'] })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation'); }); - it(`should throw an error if 'secp256k1' key pair generation fails`, async function() { - // @ts-ignore because the method is being intentionally stubbed to return null. - const secp256k1Stub = sinon.stub(Secp256k1, 'generateKeyPair').returns(Promise.resolve(null)); + it(`throws an error if 'secp256k1' key generation fails`, async function() { + // @ts-ignore because the method is being intentionally stubbed to return undefined. + const secp256k1Stub = sinon.stub(Secp256k1, 'generateKey').returns(Promise.resolve(undefined)); try { await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] + algorithm : { name: 'ECDH', curve: 'secp256k1' }, + keyOperations : ['deriveBits', 'deriveKey'] }); - secp256k1Stub.restore(); - expect.fail('Expect generateKey() to throw an error'); + expect.fail('Expected ecdh.generateKey() to throw an error'); } catch (error) { - secp256k1Stub.restore(); expect(error).to.be.an('error'); - expect((error as Error).message).to.equal('Operation failed to generate key pair.'); + expect((error as Error).message).to.include('Operation failed: generateKey'); + } finally { + secp256k1Stub.restore(); } }); - it(`should throw an error if 'X25519' key pair generation fails`, async function() { - // @ts-ignore because the method is being intentionally stubbed to return null. - const x25519Stub = sinon.stub(X25519, 'generateKeyPair').returns(Promise.resolve(null)); + it(`throws an error if 'X25519' key generation fails`, async function() { + // @ts-ignore because the method is being intentionally stubbed to return undefined. + const x25519Stub = sinon.stub(X25519, 'generateKey').returns(Promise.resolve(undefined)); try { await ecdh.generateKey({ - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : false, - keyUsages : ['deriveBits', 'deriveKey'] + algorithm : { name: 'ECDH', curve: 'X25519' }, + keyOperations : ['deriveBits', 'deriveKey'] }); - x25519Stub.restore(); - expect.fail('Expect generateKey() to throw an error'); + expect.fail('Expected ecdh.generateKey() to throw an error'); } catch (error) { - x25519Stub.restore(); expect(error).to.be.an('error'); - expect((error as Error).message).to.equal('Operation failed to generate key pair.'); + expect((error as Error).message).to.include('Operation failed: generateKey'); + } finally { + x25519Stub.restore(); } }); }); @@ -675,185 +474,102 @@ describe('Default Crypto Algorithm Implementations', () => { }); describe('generateKey()', () => { - it('returns a key pair', async () => { - const keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] + it('returns a private key in JWK format', async () => { + const privateKey = await ecdsa.generateKey({ + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['sign'] }); - expect(keys).to.have.property('privateKey'); - expect(keys.privateKey.type).to.equal('private'); - expect(keys.privateKey.usages).to.deep.equal(['sign']); + expect(privateKey).to.have.property('crv', 'secp256k1'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'EC'); + expect(privateKey).to.have.property('x'); - expect(keys).to.have.property('publicKey'); - expect(keys.publicKey.type).to.equal('public'); - expect(keys.publicKey.usages).to.deep.equal(['verify']); - }); - - it('public key is always extractable', async () => { - let keys: CryptoKeyPair; - // publicKey is extractable if generateKey() called with extractable = false - keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - expect(keys.publicKey.extractable).to.be.true; - - // publicKey is extractable if generateKey() called with extractable = true - keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - expect(keys.publicKey.extractable).to.be.true; + expect(privateKey.key_ops).to.deep.equal(['sign']); }); - it('private key is selectively extractable', async () => { - let keys: CryptoKeyPair; - // privateKey is NOT extractable if generateKey() called with extractable = false - keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - expect(keys.privateKey.extractable).to.be.false; - - // privateKey is extractable if generateKey() called with extractable = true - keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - expect(keys.privateKey.extractable).to.be.true; - }); - - it(`supports 'secp256k1' curve with compressed public keys, by default`, async () => { - const keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('compressedPublicKey' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.compressedPublicKey).to.be.true; - }); - - it(`supports 'secp256k1' curve with compressed public keys`, async () => { - const keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1', compressedPublicKey: true }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('compressedPublicKey' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.compressedPublicKey).to.be.true; - }); - - it(`supports 'secp256k1' curve with uncompressed public keys`, async () => { - const keys = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1', compressedPublicKey: false }, - extractable : false, - keyUsages : ['sign', 'verify'] + it(`supports 'secp256k1' curve`, async () => { + const privateKey = await ecdsa.generateKey({ + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['sign'] }); - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('secp256k1'); - if (!('compressedPublicKey' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.compressedPublicKey).to.be.false; + if (!('crv' in privateKey)) throw new Error; // TS type guard + expect(privateKey.crv).to.equal('secp256k1'); }); - it(`supports 'sign' and/or 'verify' key usages`, async () => { + it(`supports 'sign' and/or 'verify' key operations`, async () => { await expect(ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign'] + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['sign'] })).to.eventually.be.fulfilled; await expect(ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['verify'] + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['verify'] })).to.eventually.be.fulfilled; await expect(ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['sign', 'verify'] })).to.eventually.be.fulfilled; }); - it('validates algorithm, named curve, and key usages', async () => { - // Invalid (algorithm name, named curve, and key usages) result in algorithm name check failing first. + it('validates algorithm name and curve', async () => { + // Invalid (algorithm name, curve) results in algorithm name check failing first. await expect(ecdsa.generateKey({ - algorithm : { name: 'foo', namedCurve: 'bar' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm: { name: 'foo', curve: 'bar' } })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - // Valid (algorithm name) + Invalid (named curve, key usages) result named curve check failing first. + // Valid (algorithm name) + Invalid (curve) results in curve check failing first. await expect(ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'bar' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm: { name: 'ES256K', curve: 'bar' } })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + }); - // Valid (algorithm name, named curve) + Invalid (key usages) result key usages check failing first. - await expect(ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['encrypt'] - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation'); + it(`accepts 'keyOperations' as undefined`, async () => { + const privateKey = await ecdsa.generateKey({ + algorithm: { name: 'ES256K', curve: 'secp256k1' }, + }); + + expect(privateKey).to.exist; + expect(privateKey.key_ops).to.be.undefined; + expect(privateKey).to.have.property('kty', 'EC'); + expect(privateKey).to.have.property('crv', 'secp256k1'); }); - it(`should throw an error if 'secp256k1' key pair generation fails`, async function() { - // @ts-ignore because the method is being intentionally stubbed to return null. - const secp256k1Stub = sinon.stub(Secp256k1, 'generateKeyPair').returns(Promise.resolve(null)); + it(`throws an error if operation fails`, async function() { + const checkGenerateKeyOptionsStub = sinon.stub(ecdsa, 'checkGenerateKeyOptions').returns(undefined); try { - await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - keyUsages : ['sign'] - }); - secp256k1Stub.restore(); - expect.fail('Expect generateKey() to throw an error'); + // @ts-expect-error because no generateKey operations are defined. + await ecdsa.generateKey({ algorithm: {} }); + expect.fail('Expected ecdsa.generateKey() to throw an error'); } catch (error) { - secp256k1Stub.restore(); expect(error).to.be.an('error'); - expect((error as Error).message).to.equal('Operation failed to generate key pair.'); + expect((error as Error).message).to.include('Operation failed: generateKey'); + } finally { + checkGenerateKeyOptionsStub.restore(); } }); }); describe('sign()', () => { - - let keyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk; let data = new Uint8Array([51, 52, 53]); beforeEach(async () => { - keyPair = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] + privateKey = await ecdsa.generateKey({ + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['sign', 'verify'] }); }); it(`returns a signature for 'secp256k1' keys`, async () => { const signature = await ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.privateKey, + algorithm : { name: 'ES256K' }, + key : privateKey, data : data }); @@ -861,167 +577,80 @@ describe('Default Crypto Algorithm Implementations', () => { expect(signature.byteLength).to.equal(64); }); - it('validates algorithm name and key algorithm name', async () => { - // Invalid (algorithm name, hash algorithm, private key, and data) result in algorithm name check failing first. - await expect(ecdsa.sign({ - algorithm : { name: 'Nope', hash: 'nope' }, - // @ts-expect-error because invalid key intentionally specified. - key : { foo: 'bar '}, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - - // Valid (algorithm name) + Invalid (hash algorithm, private key, and data) result in hash algorithm check failing first. - await expect(ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'nope' }, - // @ts-expect-error because invalid key intentionally specified. - key : { foo: 'bar '}, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + it(`throws an error if sign operation fails`, async function() { + // @ts-ignore because the method is being intentionally stubbed to return undefined. + const checkSignOptionsStub = sinon.stub(ecdsa, 'checkSignOptions').returns(undefined); - // Valid (algorithm name, hash algorithm) + Invalid (private key, and data) result in key algorithm name check failing first. - await expect(ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - // @ts-expect-error because invalid key intentionally specified. - key : { algorithm: { name: 'bar '} }, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(InvalidAccessError, 'does not match'); - }); - - it('validates that key is not a public key', async () => { - // Valid (algorithm name, hash algorithm, data) + Invalid (private key) result in key type check failing first. - await expect(ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.publicKey, - data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation is not valid'); - }); - - it(`validates that key usage is 'sign'`, async () => { - // Manually specify the private key usages to exclude the 'sign' operation. - keyPair.privateKey.usages = ['verify']; - - await expect(ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.privateKey, - data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); - }); - - it('throws an error when key is an unsupported curve', async () => { - // Manually change the key's named curve to trigger an error. - // @ts-expect-error because TS can't determine the type of key. - keyPair.privateKey.algorithm.namedCurve = 'nope'; - - await expect(ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.privateKey, - data : data - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + try { + // @ts-expect-error because no sign operations are defined. + await ecdsa.sign({ algorithm: {}, key: {}, data: undefined }); + expect.fail('Expected ecdsa.sign() to throw an error'); + } catch (error) { + expect(error).to.be.an('error'); + expect((error as Error).message).to.include('Operation failed: sign'); + } finally { + checkSignOptionsStub.restore(); + } }); }); describe('verify()', () => { - let keyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk; + let publicKey: PublicKeyJwk; let signature: Uint8Array; let data = new Uint8Array([51, 52, 53]); beforeEach(async () => { - keyPair = await ecdsa.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] + privateKey = await ecdsa.generateKey({ + algorithm : { name: 'ES256K', curve: 'secp256k1' }, + keyOperations : ['sign'] }); + publicKey = await Secp256k1.computePublicKey({ privateKey }); + signature = await ecdsa.sign({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.privateKey, + algorithm : { name: 'ES256K' }, + key : privateKey, data : data }); }); - it(`returns a verification result for 'secp256k1' keys`, async () => { + it(`returns a boolean verification result`, async () => { const isValid = await ecdsa.verify({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.publicKey, + algorithm : { name: 'ES256K' }, + key : publicKey, signature : signature, data : data }); expect(isValid).to.be.a('boolean'); - expect(isValid).to.be.true; }); - it('validates algorithm name and key algorithm name', async () => { - // Invalid (algorithm name, hash algorithm, public key, signature, and data) result in algorithm name check failing first. - await expect(ecdsa.verify({ - algorithm : { name: 'Nope', hash: 'nope' }, - // @ts-expect-error because invalid key intentionally specified. - key : { foo: 'bar '}, - // @ts-expect-error because invalid signature intentionally specified. - signature : 57, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - - // Valid (algorithm name) + Invalid (hash algorithm, public key, signature and data) result in hash algorithm check failing first. - await expect(ecdsa.verify({ - algorithm : { name: 'ECDSA', hash: 'nope' }, - // @ts-expect-error because invalid key intentionally specified. - key : { foo: 'bar '}, - // @ts-expect-error because invalid signature intentionally specified. - signature : 57, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); - - // Valid (algorithm name, hash algorithm) + Invalid (public key, signature, and data) result in key algorithm name check failing first. - await expect(ecdsa.verify({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - // @ts-expect-error because invalid key intentionally specified. - key : { algorithm: { name: 'bar '} }, - // @ts-expect-error because invalid signature intentionally specified. - signature : 57, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(InvalidAccessError, 'does not match'); - }); - - it('validates that key is not a private key', async () => { - // Valid (algorithm name, hash algorithm, signature, data) + Invalid (public key) result in key type check failing first. - await expect(ecdsa.verify({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.privateKey, + it(`validates 'secp256k1' signatures`, async () => { + const isValid = await ecdsa.verify({ + algorithm : { name: 'ES256K' }, + key : publicKey, signature : signature, data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation is not valid'); - }); - - it(`validates that key usage is 'verify'`, async () => { - // Manually specify the private key usages to exclude the 'verify' operation. - keyPair.publicKey.usages = ['sign']; + }); - await expect(ecdsa.verify({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.publicKey, - signature : signature, - data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); + expect(isValid).to.be.true; }); - it('throws an error when key is an unsupported curve', async () => { - // Manually change the key's named curve to trigger an error. - // @ts-expect-error because TS can't determine the type of key. - keyPair.publicKey.algorithm.namedCurve = 'nope'; + it(`throws an error if verify operation fails`, async function() { + // @ts-ignore because the method is being intentionally stubbed to return undefined. + const checkVerifyOptionsStub = sinon.stub(ecdsa, 'checkVerifyOptions').returns(undefined); - await expect(ecdsa.verify({ - algorithm : { name: 'ECDSA', hash: 'SHA-256' }, - key : keyPair.publicKey, - signature : signature, - data : data - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + try { + // @ts-expect-error because no verify operations are defined. + await ecdsa.verify({ algorithm: {}, key: {}, data: undefined, signature: undefined }); + expect.fail('Expected ecdsa.verify() to throw an error'); + } catch (error) { + expect(error).to.be.an('error'); + expect((error as Error).message).to.include('Operation failed: verify'); + } finally { + checkVerifyOptionsStub.restore(); + } }); }); }); @@ -1034,153 +663,102 @@ describe('Default Crypto Algorithm Implementations', () => { }); describe('generateKey()', () => { - it('returns a key pair', async () => { - const keys = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] + it('returns a private key in JWK format', async () => { + const privateKey = await eddsa.generateKey({ + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['sign'] }); - expect(keys).to.have.property('privateKey'); - expect(keys.privateKey.type).to.equal('private'); - expect(keys.privateKey.usages).to.deep.equal(['sign']); + expect(privateKey).to.have.property('crv', 'Ed25519'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('x'); - expect(keys).to.have.property('publicKey'); - expect(keys.publicKey.type).to.equal('public'); - expect(keys.publicKey.usages).to.deep.equal(['verify']); - }); - - it('public key is always extractable', async () => { - let keys: CryptoKeyPair; - // publicKey is extractable if generateKey() called with extractable = false - keys = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - expect(keys.publicKey.extractable).to.be.true; - - // publicKey is extractable if generateKey() called with extractable = true - keys = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - expect(keys.publicKey.extractable).to.be.true; - }); - - it('private key is selectively extractable', async () => { - let keys: CryptoKeyPair; - // privateKey is NOT extractable if generateKey() called with extractable = false - keys = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - expect(keys.privateKey.extractable).to.be.false; - - // privateKey is extractable if generateKey() called with extractable = true - keys = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : true, - keyUsages : ['sign', 'verify'] - }); - expect(keys.privateKey.extractable).to.be.true; + expect(privateKey.key_ops).to.deep.equal(['sign']); }); it(`supports 'Ed25519' curve`, async () => { - const keys = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] + const privateKey = await eddsa.generateKey({ + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['sign'] }); - if (!('namedCurve' in keys.privateKey.algorithm)) throw new Error; // type guard - expect(keys.privateKey.algorithm.namedCurve).to.equal('Ed25519'); - if (!('namedCurve' in keys.publicKey.algorithm)) throw new Error; // type guard - expect(keys.publicKey.algorithm.namedCurve).to.equal('Ed25519'); + if (!('crv' in privateKey)) throw new Error; // TS type guard + expect(privateKey.crv).to.equal('Ed25519'); }); - it(`supports 'sign' and/or 'verify' key usages`, async () => { + it(`supports 'sign' and/or 'verify' key operations`, async () => { await expect(eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign'] + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['sign'] })).to.eventually.be.fulfilled; await expect(eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['verify'] + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['verify'] })).to.eventually.be.fulfilled; await expect(eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['sign', 'verify'] })).to.eventually.be.fulfilled; }); - it('validates algorithm, named curve, and key usages', async () => { - // Invalid (algorithm name, named curve, and key usages) result in algorithm name check failing first. + it('validates algorithm name and curve', async () => { + // Invalid (algorithm name, curve) results in algorithm name check failing first. await expect(eddsa.generateKey({ - algorithm : { name: 'foo', namedCurve: 'bar' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm: { name: 'foo', curve: 'bar' } })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - // Valid (algorithm name) + Invalid (named curve, key usages) result named curve check failing first. + // Valid (algorithm name) + Invalid (curve) results in curve check failing first. await expect(eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'bar' }, - extractable : false, - keyUsages : ['encrypt'] + algorithm: { name: 'EdDSA', curve: 'bar' } })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + }); - // Valid (algorithm name, named curve) + Invalid (key usages) result key usages check failing first. - await expect(eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['encrypt'] - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation'); + it(`accepts 'keyOperations' as undefined`, async () => { + const privateKey = await eddsa.generateKey({ + algorithm: { name: 'EdDSA', curve: 'Ed25519' }, + }); + + expect(privateKey).to.exist; + expect(privateKey.key_ops).to.be.undefined; + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('crv', 'Ed25519'); }); - it(`should throw an error if 'Ed25519' key pair generation fails`, async function() { - // @ts-ignore because the method is being intentionally stubbed to return null. - const ed25519Stub = sinon.stub(Ed25519, 'generateKeyPair').returns(Promise.resolve(null)); + it(`throws an error if operation fails`, async function() { + const checkGenerateKeyOptionsStub = sinon.stub(eddsa, 'checkGenerateKeyOptions').returns(undefined); try { - await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - ed25519Stub.restore(); - expect.fail('Expect generateKey() to throw an error'); + // @ts-expect-error because no generateKey operations are defined. + await eddsa.generateKey({ algorithm: {} }); + expect.fail('Expected eddsa.generateKey() to throw an error'); } catch (error) { - ed25519Stub.restore(); expect(error).to.be.an('error'); - expect((error as Error).message).to.equal('Operation failed to generate key pair.'); + expect((error as Error).message).to.include('Operation failed: generateKey'); + } finally { + checkGenerateKeyOptionsStub.restore(); } }); }); describe('sign()', () => { - - let keyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk; let data = new Uint8Array([51, 52, 53]); beforeEach(async () => { - keyPair = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] + privateKey = await eddsa.generateKey({ + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['sign', 'verify'] }); }); it(`returns a signature for 'Ed25519' keys`, async () => { const signature = await eddsa.sign({ algorithm : { name: 'EdDSA' }, - key : keyPair.privateKey, + key : privateKey, data : data }); @@ -1188,147 +766,80 @@ describe('Default Crypto Algorithm Implementations', () => { expect(signature.byteLength).to.equal(64); }); - it('validates algorithm name and key algorithm name', async () => { - // Invalid (algorithm name, private key, and data) result in algorithm name check failing first. - await expect(eddsa.sign({ - algorithm : { name: 'Nope' }, - // @ts-expect-error because invalid key intentionally specified. - key : { foo: 'bar '}, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - - // Valid (algorithm name) + Invalid (private key, and data) result in key algorithm name check failing first. - await expect(eddsa.sign({ - algorithm : { name: 'EdDSA' }, - // @ts-expect-error because invalid key intentionally specified. - key : { algorithm: { name: 'bar '} }, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(InvalidAccessError, 'does not match'); - }); - - it('validates that key is not a public key', async () => { - // Valid (algorithm name, data) + Invalid (private key) result in key type check failing first. - await expect(eddsa.sign({ - algorithm : { name: 'EdDSA' }, - key : keyPair.publicKey, - data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation is not valid'); - }); - - it(`validates that key usage is 'sign'`, async () => { - // Manually specify the private key usages to exclude the 'sign' operation. - keyPair.privateKey.usages = ['verify']; + it(`throws an error if sign operation fails`, async function() { + // @ts-ignore because the method is being intentionally stubbed to return undefined. + const checkSignOptionsStub = sinon.stub(eddsa, 'checkSignOptions').returns(undefined); - await expect(eddsa.sign({ - algorithm : { name: 'EdDSA' }, - key : keyPair.privateKey, - data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); - }); - - it('throws an error when key is an unsupported curve', async () => { - // Manually change the key's named curve to trigger an error. - // @ts-expect-error because TS can't determine the type of key. - keyPair.privateKey.algorithm.namedCurve = 'nope'; - - await expect(eddsa.sign({ - algorithm : { name: 'EdDSA' }, - key : keyPair.privateKey, - data : data - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + try { + // @ts-expect-error because no sign operations are defined. + await eddsa.sign({ algorithm: {}, key: {}, data: undefined }); + expect.fail('Expected eddsa.sign() to throw an error'); + } catch (error) { + expect(error).to.be.an('error'); + expect((error as Error).message).to.include('Operation failed: sign'); + } finally { + checkSignOptionsStub.restore(); + } }); }); describe('verify()', () => { - let keyPair: Web5Crypto.CryptoKeyPair; + let privateKey: PrivateKeyJwk; + let publicKey: PublicKeyJwk; let signature: Uint8Array; let data = new Uint8Array([51, 52, 53]); beforeEach(async () => { - keyPair = await eddsa.generateKey({ - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : false, - keyUsages : ['sign', 'verify'] + privateKey = await eddsa.generateKey({ + algorithm : { name: 'EdDSA', curve: 'Ed25519' }, + keyOperations : ['sign'] }); + publicKey = await Ed25519.computePublicKey({ privateKey }); + signature = await eddsa.sign({ algorithm : { name: 'EdDSA' }, - key : keyPair.privateKey, + key : privateKey, data : data }); }); - it(`returns a verification result for 'Ed25519' keys`, async () => { + it(`returns a boolean verification result`, async () => { const isValid = await eddsa.verify({ algorithm : { name: 'EdDSA' }, - key : keyPair.publicKey, + key : publicKey, signature : signature, data : data }); expect(isValid).to.be.a('boolean'); - expect(isValid).to.be.true; }); - it('validates algorithm name and key algorithm name', async () => { - // Invalid (algorithm name, public key, signature, and data) result in algorithm name check failing first. - await expect(eddsa.verify({ - algorithm : { name: 'Nope' }, - // @ts-expect-error because invalid key intentionally specified. - key : { foo: 'bar '}, - // @ts-expect-error because invalid signature intentionally specified. - signature : 57, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(NotSupportedError, 'Algorithm not supported'); - - // Valid (algorithm name) + Invalid (public key, signature, and data) result in key algorithm name check failing first. - await expect(eddsa.verify({ - algorithm : { name: 'EdDSA' }, - // @ts-expect-error because invalid key intentionally specified. - key : { algorithm: { name: 'bar '} }, - // @ts-expect-error because invalid signature intentionally specified. - signature : 57, - // @ts-expect-error because invalid data type intentionally specified. - data : 'baz' - })).to.eventually.be.rejectedWith(InvalidAccessError, 'does not match'); - }); - - it('validates that key is not a private key', async () => { - // Valid (algorithm name, signature, data) + Invalid (public key) result in key type check failing first. - await expect(eddsa.verify({ + it(`validates 'secp256k1' signatures`, async () => { + const isValid = await eddsa.verify({ algorithm : { name: 'EdDSA' }, - key : keyPair.privateKey, + key : publicKey, signature : signature, data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'Requested operation is not valid'); - }); - - it(`validates that key usage is 'verify'`, async () => { - // Manually specify the private key usages to exclude the 'verify' operation. - keyPair.publicKey.usages = ['sign']; + }); - await expect(eddsa.verify({ - algorithm : { name: 'EdDSA' }, - key : keyPair.publicKey, - signature : signature, - data : data - })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); + expect(isValid).to.be.true; }); - it('throws an error when key is an unsupported curve', async () => { - // Manually change the key's named curve to trigger an error. - // @ts-expect-error because TS can't determine the type of key. - keyPair.publicKey.algorithm.namedCurve = 'nope'; + it(`throws an error if verify operation fails`, async function() { + // @ts-ignore because the method is being intentionally stubbed to return undefined. + const checkVerifyOptionsStub = sinon.stub(eddsa, 'checkVerifyOptions').returns(undefined); - await expect(eddsa.verify({ - algorithm : { name: 'EdDSA' }, - key : keyPair.publicKey, - signature : signature, - data : data - })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); + try { + // @ts-expect-error because no verify operations are defined. + await eddsa.verify({ algorithm: {}, key: {}, data: undefined, signature: undefined }); + expect.fail('Expected eddsa.verify() to throw an error'); + } catch (error) { + expect(error).to.be.an('error'); + expect((error as Error).message).to.include('Operation failed: verify'); + } finally { + checkVerifyOptionsStub.restore(); + } }); }); }); @@ -1341,16 +852,13 @@ describe('Default Crypto Algorithm Implementations', () => { }); describe('deriveBits()', () => { - let inputKey: Web5Crypto.CryptoKey; + let inputKey: PrivateKeyJwk; beforeEach(async () => { - inputKey = await pbkdf2.importKey({ - format : 'raw', - keyData : new Uint8Array([51, 52, 53]), - algorithm : { name: 'PBKDF2' }, - extractable : false, - keyUsages : ['deriveBits'] - }); + inputKey = { + kty : 'oct', + k : Convert.uint8Array(new Uint8Array([51, 52, 53])).toBase64Url() + }; }); it('returns derived key as a Uint8Array', async () => { @@ -1387,6 +895,15 @@ describe('Default Crypto Algorithm Implementations', () => { expect(derivedKey.byteLength).to.equal(1024 / 8); }); + it('does not throw if the specified key operations are valid', async () => { + inputKey.key_ops = ['deriveBits']; + await expect(pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + })).to.eventually.be.fulfilled; + }); + it('throws error if requested length is 0', async () => { await expect(pbkdf2.deriveBits({ algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, @@ -1403,6 +920,15 @@ describe('Default Crypto Algorithm Implementations', () => { })).to.eventually.be.rejectedWith(OperationError, `'length' must be a multiple of 8`); }); + it('throws an error if the specified key operations are invalid', async () => { + inputKey.key_ops = ['encrypt', 'sign']; + await expect(pbkdf2.deriveBits({ + algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, + baseKey : inputKey, + length : 256 + })).to.eventually.be.rejectedWith(InvalidAccessError, 'is not valid for the provided key'); + }); + it(`supports 'SHA-256' hash function`, async () => { const derivedKey = await pbkdf2.deriveBits({ algorithm : { name: 'PBKDF2', hash: 'SHA-256', salt: new Uint8Array([54, 55, 56]), iterations: 1 }, @@ -1441,37 +967,5 @@ describe('Default Crypto Algorithm Implementations', () => { })).to.eventually.be.rejectedWith(TypeError, 'Out of range'); }); }); - - describe('importKey()', () => { - it('should import a key when all parameters are valid', async () => { - const key = await pbkdf2.importKey({ - format : 'raw', - keyData : new Uint8Array(16), - algorithm : { name: 'PBKDF2' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - - expect(key).to.exist; - }); - - it('should return a Web5Crypto.CryptoKey object', async () => { - const key = await pbkdf2.importKey({ - format : 'raw', - keyData : new Uint8Array(16), - algorithm : { name: 'PBKDF2' }, - extractable : false, - keyUsages : ['deriveBits'] - }); - - expect(key).to.be.an('object'); - expect(key).to.have.property('algorithm').that.is.an('object'); - expect(key.algorithm).to.have.property('name', 'PBKDF2'); - expect(key).to.have.property('extractable', false); - expect(key).to.have.property('type', 'secret'); - expect(key).to.have.property('usages').that.includes.members(['deriveBits']); - expect(key).to.have.property('material').that.is.instanceOf(Uint8Array); - }); - }); }); }); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives.spec.ts b/packages/crypto/tests/crypto-primitives.spec.ts deleted file mode 100644 index 5c3c7d40d..000000000 --- a/packages/crypto/tests/crypto-primitives.spec.ts +++ /dev/null @@ -1,1272 +0,0 @@ -import type { BytesKeyPair } from '../src/types/crypto-key.js'; - -import sinon from 'sinon'; -import chai, { expect } from 'chai'; -import { Convert } from '@web5/common'; -import chaiAsPromised from 'chai-as-promised'; - -import { NotSupportedError } from '../src/algorithms-api/errors.js'; -import { ed25519TestVectors } from './fixtures/test-vectors/ed25519.js'; -import { secp256k1TestVectors } from './fixtures/test-vectors/secp256k1.js'; -import { aesCtrTestVectors, aesGcmTestVectors } from './fixtures/test-vectors/aes.js'; -import { - AesCtr, - AesGcm, - ConcatKdf, - Ed25519, - Pbkdf2, - Secp256k1, - X25519, - XChaCha20, - XChaCha20Poly1305 -} from '../src/crypto-primitives/index.js'; - -chai.use(chaiAsPromised); - -// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage -// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule -import { webcrypto } from 'node:crypto'; -// @ts-ignore -if (!globalThis.crypto) globalThis.crypto = webcrypto; - -describe('Cryptographic Primitive Implementations', () => { - - describe('AesCtr', () => { - describe('decrypt', () => { - for (const vector of aesCtrTestVectors) { - it(`passes test vector ${vector.id}`, async () => { - const plaintext = await AesCtr.decrypt({ - counter : Convert.hex(vector.counter).toUint8Array(), - data : Convert.hex(vector.ciphertext).toUint8Array(), - key : Convert.hex(vector.key).toUint8Array(), - length : vector.length - }); - expect(Convert.uint8Array(plaintext).toHex()).to.deep.equal(vector.data); - }); - } - - it('accepts ciphertext input as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const secretKey = await AesCtr.generateKey({ length: 256 }); - let ciphertext: Uint8Array; - - // TypedArray - Uint8Array - ciphertext = await AesCtr.decrypt({ counter: new Uint8Array(16), data, key: secretKey, length: 128 }); - expect(ciphertext).to.be.instanceOf(Uint8Array); - }); - }); - - describe('encrypt', () => { - for (const vector of aesCtrTestVectors) { - it(`passes test vector ${vector.id}`, async () => { - const ciphertext = await AesCtr.encrypt({ - counter : Convert.hex(vector.counter).toUint8Array(), - data : Convert.hex(vector.data).toUint8Array(), - key : Convert.hex(vector.key).toUint8Array(), - length : vector.length - }); - expect(Convert.uint8Array(ciphertext).toHex()).to.deep.equal(vector.ciphertext); - }); - } - - it('accepts plaintext input as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const secretKey = await AesCtr.generateKey({ length: 256 }); - let ciphertext: Uint8Array; - - // Uint8Array - ciphertext = await AesCtr.encrypt({ counter: new Uint8Array(16), data, key: secretKey, length: 128 }); - expect(ciphertext).to.be.instanceOf(Uint8Array); - }); - }); - - describe('generateKey()', () => { - it('returns a secret key of type Uint8Array', async () => { - const secretKey = await AesCtr.generateKey({ length: 256 }); - expect(secretKey).to.be.instanceOf(Uint8Array); - }); - - it('returns a secret key of the specified length', async () => { - let secretKey: Uint8Array; - - // 128 bits - secretKey= await AesCtr.generateKey({ length: 128 }); - expect(secretKey.byteLength).to.equal(16); - - // 192 bits - secretKey= await AesCtr.generateKey({ length: 192 }); - expect(secretKey.byteLength).to.equal(24); - - // 256 bits - secretKey= await AesCtr.generateKey({ length: 256 }); - expect(secretKey.byteLength).to.equal(32); - }); - }); - }); - - describe('AesGcm', () => { - describe('decrypt', () => { - for (const vector of aesGcmTestVectors) { - it(`passes test vector ${vector.id}`, async () => { - const plaintext = await AesGcm.decrypt({ - additionalData : Convert.hex(vector.aad).toUint8Array(), - iv : Convert.hex(vector.iv).toUint8Array(), - data : Convert.hex(vector.ciphertext + vector.tag).toUint8Array(), - key : Convert.hex(vector.key).toUint8Array(), - tagLength : vector.tagLength - }); - expect(Convert.uint8Array(plaintext).toHex()).to.deep.equal(vector.data); - }); - } - - it('accepts ciphertext input as Uint8Array', async () => { - const secretKey = new Uint8Array([222, 78, 162, 222, 38, 146, 151, 191, 191, 75, 227, 71, 220, 221, 70, 49]); - let plaintext: Uint8Array; - - // TypedArray - Uint8Array - const ciphertext = new Uint8Array([242, 126, 129, 170, 99, 195, 21, 165, 205, 3, 226, 171, 203, 198, 42, 86, 101]); - plaintext = await AesGcm.decrypt({ data: ciphertext, iv: new Uint8Array(12), key: secretKey, tagLength: 128 }); - expect(plaintext).to.be.instanceOf(Uint8Array); - }); - }); - - describe('encrypt', () => { - for (const vector of aesGcmTestVectors) { - it(`passes test vector ${vector.id}`, async () => { - const ciphertext = await AesGcm.encrypt({ - additionalData : Convert.hex(vector.aad).toUint8Array(), - iv : Convert.hex(vector.iv).toUint8Array(), - data : Convert.hex(vector.data).toUint8Array(), - key : Convert.hex(vector.key).toUint8Array(), - tagLength : vector.tagLength - }); - expect(Convert.uint8Array(ciphertext).toHex()).to.deep.equal(vector.ciphertext + vector.tag); - }); - } - - it('accepts plaintext input as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const secretKey = await AesGcm.generateKey({ length: 256 }); - let ciphertext: Uint8Array; - - // TypedArray - Uint8Array - ciphertext = await AesGcm.encrypt({ data, iv: new Uint8Array(12), key: secretKey, tagLength: 128 }); - expect(ciphertext).to.be.instanceOf(Uint8Array); - }); - }); - - describe('generateKey()', () => { - it('returns a secret key of type Uint8Array', async () => { - const secretKey = await AesGcm.generateKey({ length: 256 }); - expect(secretKey).to.be.instanceOf(Uint8Array); - }); - - it('returns a secret key of the specified length', async () => { - let secretKey: Uint8Array; - - // 128 bits - secretKey= await AesGcm.generateKey({ length: 128 }); - expect(secretKey.byteLength).to.equal(16); - - // 192 bits - secretKey= await AesGcm.generateKey({ length: 192 }); - expect(secretKey.byteLength).to.equal(24); - - // 256 bits - secretKey= await AesGcm.generateKey({ length: 256 }); - expect(secretKey.byteLength).to.equal(32); - }); - }); - }); - - describe('ConcatKdf', () => { - describe('deriveKey()', () => { - it('matches RFC 7518 ECDH-ES key agreement computation example', async () => { - // Test vector 1 - const inputSharedSecret = 'nlbZHYFxNdNyg0KDv4QmnPsxbqPagGpI9tqneYz-kMQ'; - const input = { - sharedSecret : Convert.base64Url(inputSharedSecret).toUint8Array(), - keyDataLen : 128, - otherInfo : { - algorithmId : 'A128GCM', - partyUInfo : 'Alice', - partyVInfo : 'Bob', - suppPubInfo : 128 - } - }; - const output = 'VqqN6vgjbSBcIijNcacQGg'; - - const derivedKeyingMaterial = await ConcatKdf.deriveKey(input); - - const expectedResult = Convert.base64Url(output).toUint8Array(); - expect(derivedKeyingMaterial).to.deep.equal(expectedResult); - expect(derivedKeyingMaterial.byteLength).to.equal(16); - }); - - it('accepts other info as String and TypedArray', async () => { - const inputBase = { - sharedSecret : new Uint8Array([1, 2, 3]), - keyDataLen : 256, - otherInfo : {} - }; - - // String input. - const inputString = { ...inputBase, otherInfo: { - algorithmId : 'A128GCM', - partyUInfo : 'Alice', - partyVInfo : 'Bob', - suppPubInfo : 128 - }}; - let derivedKeyingMaterial = await ConcatKdf.deriveKey(inputString); - expect(derivedKeyingMaterial).to.be.an('Uint8Array'); - expect(derivedKeyingMaterial.byteLength).to.equal(32); - - // TypedArray input. - const inputTypedArray = { ...inputBase, otherInfo: { - algorithmId : 'A128GCM', - partyUInfo : Convert.string('Alice').toUint8Array(), - partyVInfo : Convert.string('Bob').toUint8Array(), - suppPubInfo : 128 - }}; - derivedKeyingMaterial = await ConcatKdf.deriveKey(inputTypedArray); - expect(derivedKeyingMaterial).to.be.an('Uint8Array'); - expect(derivedKeyingMaterial.byteLength).to.equal(32); - }); - - it('throws error if multi-round Concat KDF attempted', async () => { - await expect( - // @ts-expect-error because only parameters needed to trigger the error are specified. - ConcatKdf.deriveKey({ keyDataLen: 512 }) - ).to.eventually.be.rejectedWith(NotSupportedError, 'rounds not supported'); - }); - - it('throws an error if suppPubInfo is not a Number', async () => { - await expect( - ConcatKdf.deriveKey({ - sharedSecret : new Uint8Array([1, 2, 3]), - keyDataLen : 128, - otherInfo : { - algorithmId : 'A128GCM', - partyUInfo : 'Alice', - partyVInfo : 'Bob', - // @ts-expect-error because a string is specified to trigger an error. - suppPubInfo : '128', - } - }) - ).to.eventually.be.rejectedWith(TypeError, 'Fixed length input must be a number'); - }); - }); - - describe('computeOtherInfo()', () => { - it('returns concatenated and formatted Uint8Array', () => { - const input = { - algorithmId : 'A128GCM', - partyUInfo : 'Alice', - partyVInfo : 'Bob', - suppPubInfo : 128, - suppPrivInfo : 'gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0' - }; - const output = 'AAAAB0ExMjhHQ00AAAAFQWxpY2UAAAADQm9iAAAAgAAAACtnSTBHQUlMQmR1N1Q1M2FrckZtTXlHY3NGM241ZE83TW13TkJIS1c1U1Yw'; - - // @ts-expect-error because computeOtherInfo() is a private method. - const otherInfo = ConcatKdf.computeOtherInfo(input); - - const expectedResult = Convert.base64Url(output).toUint8Array(); - expect(otherInfo).to.deep.equal(expectedResult); - }); - - it('matches RFC 7518 ECDH-ES key agreement computation example', async () => { - // Test vector 1. - const input = { - algorithmId : 'A128GCM', - partyUInfo : 'Alice', - partyVInfo : 'Bob', - suppPubInfo : 128 - }; - const output = 'AAAAB0ExMjhHQ00AAAAFQWxpY2UAAAADQm9iAAAAgA'; - - // @ts-expect-error because computeOtherInfo() is a private method. - const otherInfo = ConcatKdf.computeOtherInfo(input); - - const expectedResult = Convert.base64Url(output).toUint8Array(); - expect(otherInfo).to.deep.equal(expectedResult); - }); - }); - }); - - describe('Ed25519', () => { - describe('convertPrivateKeyToX25519()', () => { - let validEd25519PrivateKey: Uint8Array; - - // This is a setup step. Before each test, generate a new Ed25519 key pair - // and use the private key in tests. This ensures that we work with fresh keys for every test. - beforeEach(async () => { - const keyPair = await Ed25519.generateKeyPair(); - validEd25519PrivateKey = keyPair.publicKey; - }); - - it('converts a valid Ed25519 private key to X25519 format', async () => { - const x25519PrivateKey = await Ed25519.convertPrivateKeyToX25519({ privateKey: validEd25519PrivateKey }); - - expect(x25519PrivateKey).to.be.instanceOf(Uint8Array); - expect(x25519PrivateKey.length).to.equal(32); - }); - - it('accepts any Uint8Array value as a private key', async () => { - /** For Ed25519 the private key is a random string of bytes that is - * hashed, which means many possible values can serve as a valid - * private key. */ - const key0Bytes = new Uint8Array(0); - const key33Bytes = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); - - let x25519PrivateKey = await Ed25519.convertPrivateKeyToX25519({ privateKey: key0Bytes }); - expect(x25519PrivateKey.length).to.equal(32); - x25519PrivateKey = await Ed25519.convertPrivateKeyToX25519({ privateKey: key33Bytes }); - expect(x25519PrivateKey.length).to.equal(32); - }); - }); - - describe('convertPublicKeyToX25519()', () => { - let validEd25519PublicKey: Uint8Array; - - // This is a setup step. Before each test, generate a new Ed25519 key pair - // and use the public key in tests. This ensures that we work with fresh keys for every test. - beforeEach(async () => { - const keyPair = await Ed25519.generateKeyPair(); - validEd25519PublicKey = keyPair.publicKey; - }); - - it('converts a valid Ed25519 public key to X25519 format', async () => { - const x25519PublicKey = await Ed25519.convertPublicKeyToX25519({ publicKey: validEd25519PublicKey }); - - expect(x25519PublicKey).to.be.instanceOf(Uint8Array); - expect(x25519PublicKey.length).to.equal(32); - }); - - it('throws an error when provided an invalid Ed25519 public key', async () => { - const invalidEd25519PublicKey = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); - - await expect( - Ed25519.convertPublicKeyToX25519({ publicKey: invalidEd25519PublicKey }) - ).to.eventually.be.rejectedWith(Error, 'Invalid public key'); - }); - - it('throws an error when provided an Ed25519 private key', async () => { - for (const vector of ed25519TestVectors) { - const validEd25519PrivateKey = Convert.hex(vector.privateKey.encoded).toUint8Array(); - - await expect( - Ed25519.convertPublicKeyToX25519({ publicKey: validEd25519PrivateKey }) - ).to.eventually.be.rejectedWith(Error, 'Invalid public key'); - } - }); - }); - - describe('generateKeyPair()', () => { - it('returns a pair of keys of type Uint8Array', async () => { - const keyPair = await Ed25519.generateKeyPair(); - expect(keyPair).to.have.property('privateKey'); - expect(keyPair).to.have.property('publicKey'); - expect(keyPair.privateKey).to.be.instanceOf(Uint8Array); - expect(keyPair.publicKey).to.be.instanceOf(Uint8Array); - }); - - it('returns a 32-byte private key', async () => { - const keyPair = await Ed25519.generateKeyPair(); - expect(keyPair.privateKey.byteLength).to.equal(32); - }); - - it('returns a 32-byte compressed public key', async () => { - const keyPair = await Ed25519.generateKeyPair(); - expect(keyPair.publicKey.byteLength).to.equal(32); - }); - }); - - describe('getPublicKey()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await Ed25519.generateKeyPair(); - }); - - it('returns a 32-byte compressed public key', async () => { - const publicKey = await Ed25519.getPublicKey({ privateKey: keyPair.privateKey }); - expect(publicKey).to.be.instanceOf(Uint8Array); - expect(publicKey.byteLength).to.equal(32); - }); - }); - - describe('sign()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await Ed25519.generateKeyPair(); - }); - - it('returns a 64-byte signature of type Uint8Array', async () => { - const data = new Uint8Array([51, 52, 53]); - const signature = await Ed25519.sign({ key: keyPair.privateKey, data }); - expect(signature).to.be.instanceOf(Uint8Array); - expect(signature.byteLength).to.equal(64); - }); - - it('accepts input data as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const key = keyPair.privateKey; - let signature: Uint8Array; - - // TypedArray - Uint8Array - signature = await Ed25519.sign({ key, data: data }); - expect(signature).to.be.instanceOf(Uint8Array); - }); - }); - - describe('validatePublicKey()', () => { - it('returns true for valid public keys', async () => { - for (const vector of ed25519TestVectors) { - const key = Convert.hex(vector.publicKey.encoded).toUint8Array(); - const isValid = await Ed25519.validatePublicKey({ key }); - expect(isValid).to.be.true; - } - }); - - it('returns false for invalid public keys', async () => { - const key = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); - const isValid = await Ed25519.validatePublicKey({ key }); - expect(isValid).to.be.false; - }); - - it('returns false if a private key is given', async () => { - for (const vector of ed25519TestVectors) { - const key = Convert.hex(vector.privateKey.encoded).toUint8Array(); - const isValid = await Ed25519.validatePublicKey({ key }); - expect(isValid).to.be.false; - } - }); - }); - - describe('verify()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await Ed25519.generateKeyPair(); - }); - - it('returns a boolean result', async () => { - const data = new Uint8Array([51, 52, 53]); - const signature = await Ed25519.sign({ key: keyPair.privateKey, data }); - - const isValid = await Ed25519.verify({ key: keyPair.publicKey, signature, data }); - expect(isValid).to.exist; - expect(isValid).to.be.true; - }); - - it('accepts input data as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - let isValid: boolean; - let signature: Uint8Array; - - // TypedArray - Uint8Array - signature = await Ed25519.sign({ key: keyPair.privateKey, data }); - isValid = await Ed25519.verify({ key: keyPair.publicKey, signature, data }); - expect(isValid).to.be.true; - }); - - it('returns false if the signed data was mutated', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - let isValid: boolean; - - // Generate signature using the private key. - const signature = await Ed25519.sign({ key: keyPair.privateKey, data }); - - // Verification should return true with the data used to generate the signature. - isValid = await Ed25519.verify({ key: keyPair.publicKey, signature, data }); - expect(isValid).to.be.true; - - // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. - const mutatedData = new Uint8Array(data); - mutatedData[0] ^= 1 << 0; - - // Verification should return false if the given data does not match the data used to generate the signature. - isValid = await Ed25519.verify({ key: keyPair.publicKey, signature, data: mutatedData }); - expect(isValid).to.be.false; - }); - - it('returns false if the signature was mutated', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - let isValid: boolean; - - // Generate a new key pair. - keyPair = await Ed25519.generateKeyPair(); - - // Generate signature using the private key. - const signature = await Ed25519.sign({ key: keyPair.privateKey, data }); - - // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. - const mutatedSignature = new Uint8Array(signature); - mutatedSignature[0] ^= 1 << 0; - - // Verification should return false if the signature was modified. - isValid = await Ed25519.verify({ key: keyPair.publicKey, signature: signature, data: mutatedSignature }); - expect(isValid).to.be.false; - }); - - it('returns false with a signature generated using a different private key', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const keyPairA = await Ed25519.generateKeyPair(); - const keyPairB = await Ed25519.generateKeyPair(); - let isValid: boolean; - - // Generate a signature using the private key from key pair B. - const signatureB = await Ed25519.sign({ key: keyPairB.privateKey, data }); - - // Verification should return false with the public key from key pair A. - isValid = await Ed25519.verify({ key: keyPairA.publicKey, signature: signatureB, data }); - expect(isValid).to.be.false; - }); - }); - }); - - describe('Pbkdf2', () => { - const password = Convert.string('password').toUint8Array(); - const salt = Convert.string('salt').toUint8Array(); - const iterations = 1; - const length = 256; // 32 bytes - - describe('deriveKey', () => { - it('successfully derives a key using WebCrypto, if available', async () => { - const subtleDeriveBitsSpy = sinon.spy(crypto.subtle, 'deriveBits'); - - const derivedKey = await Pbkdf2.deriveKey({ hash: 'SHA-256', password, salt, iterations, length }); - - expect(derivedKey).to.be.instanceOf(Uint8Array); - expect(derivedKey.byteLength).to.equal(length / 8); - expect(subtleDeriveBitsSpy.called).to.be.true; - - subtleDeriveBitsSpy.restore(); - }); - - it('successfully derives a key using node:crypto when WebCrypto is not supported', async function () { - // Skip test in web browsers since node:crypto is not available. - if (typeof window !== 'undefined') this.skip(); - - // Ensure that WebCrypto is not available for this test. - sinon.stub(crypto, 'subtle').value(null); - - // @ts-expect-error because we're spying on a private method. - const nodeCryptoDeriveKeySpy = sinon.spy(Pbkdf2, 'deriveKeyWithNodeCrypto'); - - const derivedKey = await Pbkdf2.deriveKey({ hash: 'SHA-256', password, salt, iterations, length }); - - expect(derivedKey).to.be.instanceOf(Uint8Array); - expect(derivedKey.byteLength).to.equal(length / 8); - expect(nodeCryptoDeriveKeySpy.called).to.be.true; - - nodeCryptoDeriveKeySpy.restore(); - sinon.restore(); - }); - - it('derives the same value with node:crypto and WebCrypto', async function () { - // Skip test in web browsers since node:crypto is not available. - if (typeof window !== 'undefined') this.skip(); - - const options = { hash: 'SHA-256', password, salt, iterations, length }; - - // @ts-expect-error because we're testing a private method. - const webCryptoDerivedKey = await Pbkdf2.deriveKeyWithNodeCrypto(options); - // @ts-expect-error because we're testing a private method. - const nodeCryptoDerivedKey = await Pbkdf2.deriveKeyWithWebCrypto(options); - - expect(webCryptoDerivedKey).to.deep.equal(nodeCryptoDerivedKey); - }); - - const hashFunctions: ('SHA-256' | 'SHA-384' | 'SHA-512')[] = ['SHA-256', 'SHA-384', 'SHA-512']; - hashFunctions.forEach(hash => { - it(`handles ${hash} hash function`, async () => { - const options = { hash, password, salt, iterations, length }; - - const derivedKey = await Pbkdf2.deriveKey(options); - expect(derivedKey).to.be.instanceOf(Uint8Array); - expect(derivedKey.byteLength).to.equal(length / 8); - }); - }); - - it('throws an error when an invalid hash function is used with WebCrypto', async () => { - const options = { - hash: 'SHA-2' as const, password, salt, iterations, length - }; - - // @ts-expect-error for testing purposes - await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); - }); - - it('throws an error when an invalid hash function is used with node:crypto', async function () { - // Skip test in web browsers since node:crypto is not available. - if (typeof window !== 'undefined') this.skip(); - - // Ensure that WebCrypto is not available for this test. - sinon.stub(crypto, 'subtle').value(null); - - const options = { - hash: 'SHA-2' as const, password, salt, iterations, length - }; - - // @ts-expect-error for testing purposes - await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); - - sinon.restore(); - }); - - it('throws an error when iterations count is not a positive number with WebCrypto', async () => { - const options = { - hash : 'SHA-256' as const, password, salt, - iterations : -1, length - }; - - // Every browser throws a different error message so a specific message cannot be checked. - await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); - }); - - it('throws an error when iterations count is not a positive number with node:crypto', async function () { - // Skip test in web browsers since node:crypto is not available. - if (typeof window !== 'undefined') this.skip(); - - // Ensure that WebCrypto is not available for this test. - sinon.stub(crypto, 'subtle').value(null); - - const options = { - hash : 'SHA-256' as const, password, salt, - iterations : -1, length - }; - - await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error, 'out of range'); - - sinon.restore(); - }); - }); - }); - - describe('Secp256k1', () => { - describe('convertPublicKey method', () => { - it('converts an uncompressed public key to a compressed format', async () => { - // Generate the uncompressed public key. - const keyPair = await Secp256k1.generateKeyPair({ compressedPublicKey: false }); - const uncompressedPublicKey = keyPair.publicKey; - - // Attempt to convert to compressed format. - const compressedKey = await Secp256k1.convertPublicKey({ - publicKey : uncompressedPublicKey, - compressedPublicKey : true - }); - - // Confirm the length of the resulting public key is 33 bytes - expect(compressedKey.byteLength).to.equal(33); - }); - - it('converts a compressed public key to an uncompressed format', async () => { - // Generate the uncompressed public key. - const keyPair = await Secp256k1.generateKeyPair({ compressedPublicKey: true }); - const compressedPublicKey = keyPair.publicKey; - - const uncompressedKey = await Secp256k1.convertPublicKey({ - publicKey : compressedPublicKey, - compressedPublicKey : false - }); - - // Confirm the length of the resulting public key is 65 bytes - expect(uncompressedKey.byteLength).to.equal(65); - }); - - it('throws an error for an invalid compressed public key', async () => { - // Invalid compressed public key. - const invalidPublicKey = Convert.hex('fef0b998921eafb58f49efdeb0adc47123aa28a4042924236f08274d50c72fe7b0').toUint8Array(); - - try { - await Secp256k1.convertPublicKey({ - publicKey : invalidPublicKey, - compressedPublicKey : false - }); - expect.fail('Expected method to throw an error.'); - } catch (error) { - expect(error).to.be.instanceOf(Error); - expect((error as Error).message).to.include('Point of length 33 was invalid'); - } - }); - - it('throws an error for an invalid uncompressed public key', async () => { - // Here, generating a random byte array of size 65. It's unlikely to be a valid public key. - const invalidPublicKey = Convert.hex('dfebc16793a5737ac51f606a43524df8373c063e41d5a99b2f1530afd987284bd1c7cde1658a9a756e71f44a97b4783ea9dee5ccb7f1447eb4836d8de9bd4f81fd').toUint8Array(); - - try { - await Secp256k1.convertPublicKey({ - publicKey : invalidPublicKey, - compressedPublicKey : true - }); - expect.fail('Expected method to throw an error.'); - } catch (error) { - expect(error).to.be.instanceOf(Error); - expect((error as Error).message).to.include('Point of length 65 was invalid'); - } - }); - }); - - describe('generateKeyPair()', () => { - it('returns a pair of keys of type Uint8Array', async () => { - const keyPair = await Secp256k1.generateKeyPair(); - expect(keyPair).to.have.property('privateKey'); - expect(keyPair).to.have.property('publicKey'); - expect(keyPair.privateKey).to.be.instanceOf(Uint8Array); - expect(keyPair.publicKey).to.be.instanceOf(Uint8Array); - }); - - it('returns a 32-byte private key', async () => { - const keyPair = await Secp256k1.generateKeyPair(); - expect(keyPair.privateKey.byteLength).to.equal(32); - }); - - it('returns a 33-byte compressed public key, by default', async () => { - const keyPair = await Secp256k1.generateKeyPair(); - expect(keyPair.publicKey.byteLength).to.equal(33); - }); - - it('returns a 65-byte uncompressed public key, if specified', async () => { - const keyPair = await Secp256k1.generateKeyPair({ compressedPublicKey: false }); - expect(keyPair.publicKey.byteLength).to.equal(65); - }); - }); - - describe('getCurvePoints()', () => { - it('returns public key x and y coordinates given a public key', async () => { - for (const vector of secp256k1TestVectors) { - const key = Convert.hex(vector.publicKey.encoded).toUint8Array(); - const points = await Secp256k1.getCurvePoints({ key }); - expect(points.x).to.deep.equal(Convert.hex(vector.publicKey.x).toUint8Array()); - expect(points.y).to.deep.equal(Convert.hex(vector.publicKey.y).toUint8Array()); - } - }); - - it('returns public key x and y coordinates given a private key', async () => { - for (const vector of secp256k1TestVectors) { - const key = Convert.hex(vector.privateKey.encoded).toUint8Array(); - const points = await Secp256k1.getCurvePoints({ key }); - expect(points.x).to.deep.equal(Convert.hex(vector.publicKey.x).toUint8Array()); - expect(points.y).to.deep.equal(Convert.hex(vector.publicKey.y).toUint8Array()); - } - }); - - it('handles keys that require padded x-coordinate when converting from BigInt to bytes', async () => { - const key = Convert.hex('0206a1f9628c5bcd31f3bbc2f160ec98f99960147e04ea192f56c53a0086c5432d').toUint8Array(); - const points = await Secp256k1.getCurvePoints({ key }); - - const expectedX = Convert.hex('06a1f9628c5bcd31f3bbc2f160ec98f99960147e04ea192f56c53a0086c5432d').toUint8Array(); - const expectedY = Convert.hex('bf2efab7943be51219a283c0979ccba0fbe03f571e75b0eb338cc2ec01e70552').toUint8Array(); - expect(points.x).to.deep.equal(expectedX); - expect(points.y).to.deep.equal(expectedY); - }); - - it('handles keys that require padded y-coordinate when converting from BigInt to bytes', async () => { - const key = Convert.hex('032ff752fb8fc6af85c8682b0ca9d48901b2b9ac130f558bd1a9092240d42c4682').toUint8Array(); - const points = await Secp256k1.getCurvePoints({ key }); - - const expectedX = Convert.hex('2ff752fb8fc6af85c8682b0ca9d48901b2b9ac130f558bd1a9092240d42c4682').toUint8Array(); - const expectedY = Convert.hex('048c39d9ebdc1fd98bda38b7f00b93de1d2af5bb3ba8cb532ad47c1f36e19501').toUint8Array(); - expect(points.x).to.deep.equal(expectedX); - expect(points.y).to.deep.equal(expectedY); - }); - - it('throws error with invalid input key length', async () => { - await expect( - Secp256k1.getCurvePoints({ key: new Uint8Array(16) }) - ).to.eventually.be.rejectedWith(Error, 'Point of length 16 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes'); - }); - }); - - describe('getPublicKey()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await Secp256k1.generateKeyPair(); - }); - - it('returns a 33-byte compressed public key, by default', async () => { - const publicKey = await Secp256k1.getPublicKey({ privateKey: keyPair.privateKey }); - expect(publicKey).to.be.instanceOf(Uint8Array); - expect(publicKey.byteLength).to.equal(33); - }); - - it('returns a 65-byte uncompressed public key, if specified', async () => { - const publicKey = await Secp256k1.getPublicKey({ privateKey: keyPair.privateKey, compressedPublicKey: false }); - expect(publicKey).to.be.instanceOf(Uint8Array); - expect(publicKey.byteLength).to.equal(65); - }); - }); - - describe('sharedSecret()', () => { - let otherPartyKeyPair: BytesKeyPair; - let ownKeyPair: BytesKeyPair; - - beforeEach(async () => { - otherPartyKeyPair = await Secp256k1.generateKeyPair(); - ownKeyPair = await Secp256k1.generateKeyPair(); - }); - - it('generates a 32-byte shared secret', async () => { - const sharedSecret = await Secp256k1.sharedSecret({ - privateKey : ownKeyPair.privateKey, - publicKey : otherPartyKeyPair.publicKey - }); - expect(sharedSecret).to.be.instanceOf(Uint8Array); - expect(sharedSecret.byteLength).to.equal(32); - }); - - it('generates identical output if keypairs are swapped', async () => { - const sharedSecretOwnOther = await Secp256k1.sharedSecret({ - privateKey : ownKeyPair.privateKey, - publicKey : otherPartyKeyPair.publicKey - }); - - const sharedSecretOtherOwn = await Secp256k1.sharedSecret({ - privateKey : otherPartyKeyPair.privateKey, - publicKey : ownKeyPair.publicKey - }); - - expect(sharedSecretOwnOther).to.deep.equal(sharedSecretOtherOwn); - }); - }); - - describe('sign()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await Secp256k1.generateKeyPair(); - }); - - it('returns a 64-byte signature of type Uint8Array', async () => { - const hash = 'SHA-256'; - const data = new Uint8Array([51, 52, 53]); - const signature = await Secp256k1.sign({ hash, key: keyPair.privateKey, data }); - expect(signature).to.be.instanceOf(Uint8Array); - expect(signature.byteLength).to.equal(64); - }); - - it('accepts input data as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const hash = 'SHA-256'; - const key = keyPair.privateKey; - let signature: Uint8Array; - - // TypedArray - Uint8Array - signature = await Secp256k1.sign({ hash, key, data }); - expect(signature).to.be.instanceOf(Uint8Array); - }); - }); - - describe('validatePrivateKey()', () => { - it('returns true for valid private keys', async () => { - for (const vector of secp256k1TestVectors) { - const key = Convert.hex(vector.privateKey.encoded).toUint8Array(); - const isValid = await Secp256k1.validatePrivateKey({ key }); - expect(isValid).to.be.true; - } - }); - - it('returns false for invalid private keys', async () => { - const key = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); - const isValid = await Secp256k1.validatePrivateKey({ key }); - expect(isValid).to.be.false; - }); - - it('returns false if a public key is given', async () => { - for (const vector of secp256k1TestVectors) { - const key = Convert.hex(vector.publicKey.encoded).toUint8Array(); - const isValid = await Secp256k1.validatePrivateKey({ key }); - expect(isValid).to.be.false; - } - }); - }); - - describe('validatePublicKey()', () => { - it('returns true for valid public keys', async () => { - for (const vector of secp256k1TestVectors) { - const key = Convert.hex(vector.publicKey.encoded).toUint8Array(); - const isValid = await Secp256k1.validatePublicKey({ key }); - expect(isValid).to.be.true; - } - }); - - it('returns false for invalid public keys', async () => { - const key = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); - const isValid = await Secp256k1.validatePublicKey({ key }); - expect(isValid).to.be.false; - }); - - it('returns false if a private key is given', async () => { - for (const vector of secp256k1TestVectors) { - const key = Convert.hex(vector.privateKey.encoded).toUint8Array(); - const isValid = await Secp256k1.validatePublicKey({ key }); - expect(isValid).to.be.false; - } - }); - }); - - describe('verify()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await Secp256k1.generateKeyPair(); - }); - - it('returns a boolean result', async () => { - const data = new Uint8Array([51, 52, 53]); - const signature = await Secp256k1.sign({ hash: 'SHA-256', key: keyPair.privateKey, data }); - - const isValid = await Secp256k1.verify({ hash: 'SHA-256', key: keyPair.publicKey, signature, data }); - expect(isValid).to.exist; - expect(isValid).to.be.true; - }); - - it('accepts input data as Uint8Array', async () => { - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const hash = 'SHA-256'; - let isValid: boolean; - let signature: Uint8Array; - - // TypedArray - Uint8Array - signature = await Secp256k1.sign({ hash, key: keyPair.privateKey, data }); - isValid = await Secp256k1.verify({ hash, key: keyPair.publicKey, signature, data }); - expect(isValid).to.be.true; - }); - - it('accepts both compressed and uncompressed public keys', async () => { - let signature: Uint8Array; - let isValid: boolean; - const hash = 'SHA-256'; - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - - // Generate signature using the private key. - signature = await Secp256k1.sign({ hash, key: keyPair.privateKey, data }); - - // Attempt to verify the signature using a compressed public key. - const compressedPublicKey = await Secp256k1.getPublicKey({ privateKey: keyPair.privateKey, compressedPublicKey: true }); - isValid = await Secp256k1.verify({ hash, key: compressedPublicKey, signature, data }); - expect(isValid).to.be.true; - - // Attempt to verify the signature using an uncompressed public key. - const uncompressedPublicKey = await Secp256k1.getPublicKey({ privateKey: keyPair.privateKey, compressedPublicKey: false }); - isValid = await Secp256k1.verify({ hash, key: uncompressedPublicKey, signature, data }); - expect(isValid).to.be.true; - }); - - it('returns false if the signed data was mutated', async () => { - const hash = 'SHA-256'; - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - let isValid: boolean; - - // Generate signature using the private key. - const signature = await Secp256k1.sign({ hash, key: keyPair.privateKey, data }); - - // Verification should return true with the data used to generate the signature. - isValid = await Secp256k1.verify({ hash, key: keyPair.publicKey, signature, data }); - expect(isValid).to.be.true; - - // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. - const mutatedData = new Uint8Array(data); - mutatedData[0] ^= 1 << 0; - - // Verification should return false if the given data does not match the data used to generate the signature. - isValid = await Secp256k1.verify({ hash, key: keyPair.publicKey, signature, data: mutatedData }); - expect(isValid).to.be.false; - }); - - it('returns false if the signature was mutated', async () => { - const hash = 'SHA-256'; - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - let isValid: boolean; - - // Generate signature using the private key. - const signature = await Secp256k1.sign({ hash, key: keyPair.privateKey, data }); - - // Verification should return true with the data used to generate the signature. - isValid = await Secp256k1.verify({ hash, key: keyPair.publicKey, signature, data }); - expect(isValid).to.be.true; - - // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. - const mutatedSignature = new Uint8Array(signature); - mutatedSignature[0] ^= 1 << 0; - - // Verification should return false if the signature was modified. - isValid = await Secp256k1.verify({ hash, key: keyPair.publicKey, signature: signature, data: mutatedSignature }); - expect(isValid).to.be.false; - }); - - it('returns false with a signature generated using a different private key', async () => { - const hash = 'SHA-256'; - const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); - const keyPairA = await Secp256k1.generateKeyPair(); - const keyPairB = await Secp256k1.generateKeyPair(); - let isValid: boolean; - - // Generate a signature using the private key from key pair B. - const signatureB = await Secp256k1.sign({ hash, key: keyPairB.privateKey, data }); - - // Verification should return false with the public key from key pair A. - isValid = await Secp256k1.verify({ hash, key: keyPairA.publicKey, signature: signatureB, data }); - expect(isValid).to.be.false; - }); - }); - }); - - describe('X25519', () => { - describe('generateKeyPair()', () => { - it('returns a pair of keys of type Uint8Array', async () => { - const keyPair = await X25519.generateKeyPair(); - expect(keyPair).to.have.property('privateKey'); - expect(keyPair).to.have.property('publicKey'); - expect(keyPair.privateKey).to.be.instanceOf(Uint8Array); - expect(keyPair.publicKey).to.be.instanceOf(Uint8Array); - }); - - it('returns a 32-byte private key', async () => { - const keyPair = await X25519.generateKeyPair(); - expect(keyPair.privateKey.byteLength).to.equal(32); - }); - - it('returns a 32-byte compressed public key', async () => { - const keyPair = await X25519.generateKeyPair(); - expect(keyPair.publicKey.byteLength).to.equal(32); - }); - }); - - describe('getPublicKey()', () => { - let keyPair: BytesKeyPair; - - before(async () => { - keyPair = await X25519.generateKeyPair(); - }); - - it('returns a 32-byte compressed public key', async () => { - const publicKey = await X25519.getPublicKey({ privateKey: keyPair.privateKey }); - expect(publicKey).to.be.instanceOf(Uint8Array); - expect(publicKey.byteLength).to.equal(32); - }); - }); - - describe('sharedSecret()', () => { - let otherPartyKeyPair: BytesKeyPair; - let ownKeyPair: BytesKeyPair; - - beforeEach(async () => { - otherPartyKeyPair = await X25519.generateKeyPair(); - ownKeyPair = await X25519.generateKeyPair(); - }); - - it('generates a 32-byte compressed secret', async () => { - const sharedSecret = await X25519.sharedSecret({ - privateKey : ownKeyPair.privateKey, - publicKey : otherPartyKeyPair.publicKey - }); - expect(sharedSecret).to.be.instanceOf(Uint8Array); - expect(sharedSecret.byteLength).to.equal(32); - }); - - it('generates identical output if keypairs are swapped', async () => { - const sharedSecretOwnOther = await X25519.sharedSecret({ - privateKey : ownKeyPair.privateKey, - publicKey : otherPartyKeyPair.publicKey - }); - - const sharedSecretOtherOwn = await X25519.sharedSecret({ - privateKey : otherPartyKeyPair.privateKey, - publicKey : ownKeyPair.publicKey - }); - - expect(sharedSecretOwnOther).to.deep.equal(sharedSecretOtherOwn); - }); - }); - - describe('validatePublicKey()', () => { - it('throws a not implemented error', async () => { - await expect( - X25519.validatePublicKey({ key: new Uint8Array(32) }) - ).to.eventually.be.rejectedWith(Error, 'Not implemented'); - }); - }); - }); - - describe('XChaCha20', () => { - describe('decrypt()', () => { - it('returns Uint8Array plaintext with length matching input', async () => { - const plaintext = await XChaCha20.decrypt({ - data : new Uint8Array(10), - key : new Uint8Array(32), - nonce : new Uint8Array(24) - }); - expect(plaintext).to.be.an('Uint8Array'); - expect(plaintext.byteLength).to.equal(10); - }); - - it('passes test vectors', async () => { - const input = { - data : Convert.hex('879b10a139674fe65087f59577ee2c1ab54655d900697fd02d953f53ddcc1ae476e8').toUint8Array(), - key : Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(), - nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array() - }; - const output = Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(); - - const ciphertext = await XChaCha20.decrypt({ - data : input.data, - key : input.key, - nonce : input.nonce - }); - - expect(ciphertext).to.deep.equal(output); - }); - }); - - describe('encrypt()', () => { - it('returns Uint8Array ciphertext with length matching input', async () => { - const ciphertext = await XChaCha20.encrypt({ - data : new Uint8Array(10), - key : new Uint8Array(32), - nonce : new Uint8Array(24) - }); - expect(ciphertext).to.be.an('Uint8Array'); - expect(ciphertext.byteLength).to.equal(10); - }); - - it('passes test vectors', async () => { - const input = { - data : Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(), - key : Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(), - nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array() - }; - const output = Convert.hex('879b10a139674fe65087f59577ee2c1ab54655d900697fd02d953f53ddcc1ae476e8').toUint8Array(); - - const ciphertext = await XChaCha20.encrypt({ - data : input.data, - key : input.key, - nonce : input.nonce - }); - - expect(ciphertext).to.deep.equal(output); - }); - }); - - describe('generateKey()', () => { - it('returns a 32-byte secret key of type Uint8Array', async () => { - const secretKey = await XChaCha20.generateKey(); - expect(secretKey).to.be.instanceOf(Uint8Array); - expect(secretKey.byteLength).to.equal(32); - }); - }); - }); - - describe('XChaCha20Poly1305', () => { - describe('decrypt()', () => { - it('returns Uint8Array plaintext with length matching input', async () => { - const plaintext = await XChaCha20Poly1305.decrypt({ - data : Convert.hex('789e9689e5208d7fd9e1').toUint8Array(), - key : new Uint8Array(32), - nonce : new Uint8Array(24), - tag : Convert.hex('09701fb9f36ab77a0f136ca539229a34').toUint8Array() - }); - expect(plaintext).to.be.an('Uint8Array'); - expect(plaintext.byteLength).to.equal(10); - }); - - it('passes test vectors', async () => { - const input = { - data : Convert.hex('80246ca517c0fb5860c19090a7e7a2b030dde4882520102cbc64fad937916596ca9d').toUint8Array(), - key : Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(), - nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array(), - tag : Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array() - }; - const output = Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(); - - const plaintext = await XChaCha20Poly1305.decrypt({ - data : input.data, - key : input.key, - nonce : input.nonce, - tag : input.tag - }); - - expect(plaintext).to.deep.equal(output); - }); - - it('throws an error if the wrong tag is given', async () => { - await expect( - XChaCha20Poly1305.decrypt({ - data : new Uint8Array(10), - key : new Uint8Array(32), - nonce : new Uint8Array(24), - tag : new Uint8Array(16) - }) - ).to.eventually.be.rejectedWith(Error, 'Wrong tag'); - }); - }); - - describe('encrypt()', () => { - it('returns Uint8Array ciphertext and tag', async () => { - const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ - data : new Uint8Array(10), - key : new Uint8Array(32), - nonce : new Uint8Array(24) - }); - expect(ciphertext).to.be.an('Uint8Array'); - expect(ciphertext.byteLength).to.equal(10); - expect(tag).to.be.an('Uint8Array'); - expect(tag.byteLength).to.equal(16); - }); - - it('accepts additional authenticated data', async () => { - const { ciphertext: ciphertextAad, tag: tagAad } = await XChaCha20Poly1305.encrypt({ - additionalData : new Uint8Array(64), - data : new Uint8Array(10), - key : new Uint8Array(32), - nonce : new Uint8Array(24) - }); - - const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ - data : new Uint8Array(10), - key : new Uint8Array(32), - nonce : new Uint8Array(24) - }); - - expect(ciphertextAad.byteLength).to.equal(10); - expect(ciphertext.byteLength).to.equal(10); - expect(ciphertextAad).to.deep.equal(ciphertext); - expect(tagAad).to.not.deep.equal(tag); - }); - - it('passes test vectors', async () => { - const input = { - data : Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(), - key : Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(), - nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array() - }; - const output = { - ciphertext : Convert.hex('80246ca517c0fb5860c19090a7e7a2b030dde4882520102cbc64fad937916596ca9d').toUint8Array(), - tag : Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array() - }; - - const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ - data : input.data, - key : input.key, - nonce : input.nonce - }); - - expect(ciphertext).to.deep.equal(output.ciphertext); - expect(tag).to.deep.equal(output.tag); - }); - }); - - describe('generateKey()', () => { - it('returns a 32-byte secret key of type Uint8Array', async () => { - const secretKey = await XChaCha20Poly1305.generateKey(); - expect(secretKey).to.be.instanceOf(Uint8Array); - expect(secretKey.byteLength).to.equal(32); - }); - }); - }); - -}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/aes-ctr.spec.ts b/packages/crypto/tests/crypto-primitives/aes-ctr.spec.ts new file mode 100644 index 000000000..9f8fdf2a2 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/aes-ctr.spec.ts @@ -0,0 +1,153 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { JwkParamsOctPrivate, PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import { AesCtr } from '../../src/crypto-primitives/aes-ctr.js'; +import { aesCtrTestVectors } from '../fixtures/test-vectors/aes.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('AesCtr', () => { + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('ffbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + const privateKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + + it('returns the expected JWK given byte array input', async () => { + const privateKeyBytes = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + + const privateKey = await AesCtr.bytesToPrivateKey({ privateKeyBytes }); + + const expectedOutput: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + expect(privateKey).to.deep.equal(expectedOutput); + }); + }); + + describe('decrypt', () => { + for (const vector of aesCtrTestVectors) { + it(`passes test vector ${vector.id}`, async () => { + const plaintext = await AesCtr.decrypt({ + counter : Convert.hex(vector.counter).toUint8Array(), + data : Convert.hex(vector.ciphertext).toUint8Array(), + key : await AesCtr.bytesToPrivateKey({ privateKeyBytes: Convert.hex(vector.key).toUint8Array() }), + length : vector.length + }); + expect(Convert.uint8Array(plaintext).toHex()).to.deep.equal(vector.data); + }); + } + + it('accepts ciphertext input as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const privateKey = await AesCtr.generateKey({ length: 256 }); + + const ciphertext = await AesCtr.decrypt({ counter: new Uint8Array(16), data, key: privateKey, length: 128 }); + expect(ciphertext).to.be.instanceOf(Uint8Array); + }); + }); + + describe('encrypt', () => { + for (const vector of aesCtrTestVectors) { + it(`passes test vector ${vector.id}`, async () => { + const ciphertext = await AesCtr.encrypt({ + counter : Convert.hex(vector.counter).toUint8Array(), + data : Convert.hex(vector.data).toUint8Array(), + key : await AesCtr.bytesToPrivateKey({ privateKeyBytes: Convert.hex(vector.key).toUint8Array() }), + length : vector.length + }); + expect(Convert.uint8Array(ciphertext).toHex()).to.deep.equal(vector.ciphertext); + }); + } + + it('accepts plaintext input as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const privateKey = await AesCtr.generateKey({ length: 256 }); + let ciphertext: Uint8Array; + + // Uint8Array + ciphertext = await AesCtr.encrypt({ counter: new Uint8Array(16), data, key: privateKey, length: 128 }); + expect(ciphertext).to.be.instanceOf(Uint8Array); + }); + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await AesCtr.generateKey({ length: 256 }); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + + it('returns a private key of the specified length', async () => { + let privateKey: JwkParamsOctPrivate; + let privateKeyBytes: Uint8Array; + + // 128 bits + privateKey = await AesCtr.generateKey({ length: 128 }) as JwkParamsOctPrivate; + privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(16); + + // 192 bits + privateKey = await AesCtr.generateKey({ length: 192 }) as JwkParamsOctPrivate; + privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(24); + + // 256 bits + privateKey = await AesCtr.generateKey({ length: 256 }) as JwkParamsOctPrivate; + privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(32); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey = await AesCtr.generateKey({ length: 128 }); + const privateKeyBytes = await AesCtr.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + }); + + it('returns the expected byte array for JWK input', async () => { + const privateKey: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + const privateKeyBytes = await AesCtr.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an asymmetric public key', async () => { + const publicKey: PublicKeyJwk = { + crv : 'Ed25519', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + AesCtr.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid oct private key'); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/aes-gcm.spec.ts b/packages/crypto/tests/crypto-primitives/aes-gcm.spec.ts new file mode 100644 index 000000000..1f793c7e7 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/aes-gcm.spec.ts @@ -0,0 +1,156 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { JwkParamsOctPrivate, PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import { AesGcm } from '../../src/crypto-primitives/aes-gcm.js'; +import { aesGcmTestVectors } from '../fixtures/test-vectors/aes.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('AesGcm', () => { + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('ffbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + const privateKey = await AesGcm.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + + it('returns the expected JWK given byte array input', async () => { + const privateKeyBytes = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + + const privateKey = await AesGcm.bytesToPrivateKey({ privateKeyBytes }); + + const expectedOutput: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + expect(privateKey).to.deep.equal(expectedOutput); + }); + }); + + describe('decrypt', () => { + for (const vector of aesGcmTestVectors) { + it(`passes test vector ${vector.id}`, async () => { + const plaintext = await AesGcm.decrypt({ + additionalData : Convert.hex(vector.aad).toUint8Array(), + iv : Convert.hex(vector.iv).toUint8Array(), + data : Convert.hex(vector.ciphertext + vector.tag).toUint8Array(), + key : await AesGcm.bytesToPrivateKey({ privateKeyBytes: Convert.hex(vector.key).toUint8Array() }), + tagLength : vector.tagLength + }); + expect(Convert.uint8Array(plaintext).toHex()).to.deep.equal(vector.data); + }); + } + + it('accepts ciphertext input as Uint8Array', async () => { + const privateKeyBytes = Convert.hex('de4ea2de269297bfbf4be347dcdd4631').toUint8Array(); + const privateKey = await AesGcm.bytesToPrivateKey({ privateKeyBytes }); + const ciphertext = Convert.hex('f27e81aa63c315a5cd03e2abcbc62a5665').toUint8Array(); + + const plaintext = await AesGcm.decrypt({ data: ciphertext, iv: new Uint8Array(12), key: privateKey, tagLength: 128 }); + expect(plaintext).to.be.instanceOf(Uint8Array); + }); + }); + + describe('encrypt', () => { + for (const vector of aesGcmTestVectors) { + it(`passes test vector ${vector.id}`, async () => { + const ciphertext = await AesGcm.encrypt({ + additionalData : Convert.hex(vector.aad).toUint8Array(), + iv : Convert.hex(vector.iv).toUint8Array(), + data : Convert.hex(vector.data).toUint8Array(), + key : await AesGcm.bytesToPrivateKey({ privateKeyBytes: Convert.hex(vector.key).toUint8Array() }), + tagLength : vector.tagLength + }); + expect(Convert.uint8Array(ciphertext).toHex()).to.deep.equal(vector.ciphertext + vector.tag); + }); + } + + it('accepts plaintext input as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const secretKey = await AesGcm.generateKey({ length: 256 }); + let ciphertext: Uint8Array; + + // TypedArray - Uint8Array + ciphertext = await AesGcm.encrypt({ data, iv: new Uint8Array(12), key: secretKey, tagLength: 128 }); + expect(ciphertext).to.be.instanceOf(Uint8Array); + }); + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await AesGcm.generateKey({ length: 256 }); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + + it('returns a private key of the specified length', async () => { + let privateKey: JwkParamsOctPrivate; + let privateKeyBytes: Uint8Array; + + // 128 bits + privateKey = await AesGcm.generateKey({ length: 128 }) as JwkParamsOctPrivate; + privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(16); + + // 192 bits + privateKey = await AesGcm.generateKey({ length: 192 }) as JwkParamsOctPrivate; + privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(24); + + // 256 bits + privateKey = await AesGcm.generateKey({ length: 256 }) as JwkParamsOctPrivate; + privateKeyBytes = Convert.base64Url(privateKey.k).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(32); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey = await AesGcm.generateKey({ length: 128 }); + const privateKeyBytes = await AesGcm.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + }); + + it('returns the expected byte array for JWK input', async () => { + const privateKey: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + const privateKeyBytes = await AesGcm.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an asymmetric public key', async () => { + const publicKey: PublicKeyJwk = { + crv : 'Ed25519', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + AesGcm.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid oct private key'); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/concat-kdf.spec.ts b/packages/crypto/tests/crypto-primitives/concat-kdf.spec.ts new file mode 100644 index 000000000..06f5096a9 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/concat-kdf.spec.ts @@ -0,0 +1,129 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import { NotSupportedError } from '../../src/algorithms-api/errors.js'; +import { ConcatKdf } from '../../src/crypto-primitives/concat-kdf.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('ConcatKdf', () => { + describe('deriveKey()', () => { + it('matches RFC 7518 ECDH-ES key agreement computation example', async () => { + // Test vector 1 + const inputSharedSecret = 'nlbZHYFxNdNyg0KDv4QmnPsxbqPagGpI9tqneYz-kMQ'; + const input = { + sharedSecret : Convert.base64Url(inputSharedSecret).toUint8Array(), + keyDataLen : 128, + otherInfo : { + algorithmId : 'A128GCM', + partyUInfo : 'Alice', + partyVInfo : 'Bob', + suppPubInfo : 128 + } + }; + const output = 'VqqN6vgjbSBcIijNcacQGg'; + + const derivedKeyingMaterial = await ConcatKdf.deriveKey(input); + + const expectedResult = Convert.base64Url(output).toUint8Array(); + expect(derivedKeyingMaterial).to.deep.equal(expectedResult); + expect(derivedKeyingMaterial.byteLength).to.equal(16); + }); + + it('accepts other info as String and TypedArray', async () => { + const inputBase = { + sharedSecret : new Uint8Array([1, 2, 3]), + keyDataLen : 256, + otherInfo : {} + }; + + // String input. + const inputString = { ...inputBase, otherInfo: { + algorithmId : 'A128GCM', + partyUInfo : 'Alice', + partyVInfo : 'Bob', + suppPubInfo : 128 + }}; + let derivedKeyingMaterial = await ConcatKdf.deriveKey(inputString); + expect(derivedKeyingMaterial).to.be.an('Uint8Array'); + expect(derivedKeyingMaterial.byteLength).to.equal(32); + + // TypedArray input. + const inputTypedArray = { ...inputBase, otherInfo: { + algorithmId : 'A128GCM', + partyUInfo : Convert.string('Alice').toUint8Array(), + partyVInfo : Convert.string('Bob').toUint8Array(), + suppPubInfo : 128 + }}; + derivedKeyingMaterial = await ConcatKdf.deriveKey(inputTypedArray); + expect(derivedKeyingMaterial).to.be.an('Uint8Array'); + expect(derivedKeyingMaterial.byteLength).to.equal(32); + }); + + it('throws error if multi-round Concat KDF attempted', async () => { + await expect( + // @ts-expect-error because only parameters needed to trigger the error are specified. + ConcatKdf.deriveKey({ keyDataLen: 512 }) + ).to.eventually.be.rejectedWith(NotSupportedError, 'rounds not supported'); + }); + + it('throws an error if suppPubInfo is not a Number', async () => { + await expect( + ConcatKdf.deriveKey({ + sharedSecret : new Uint8Array([1, 2, 3]), + keyDataLen : 128, + otherInfo : { + algorithmId : 'A128GCM', + partyUInfo : 'Alice', + partyVInfo : 'Bob', + // @ts-expect-error because a string is specified to trigger an error. + suppPubInfo : '128', + } + }) + ).to.eventually.be.rejectedWith(TypeError, 'Fixed length input must be a number'); + }); + }); + + describe('computeOtherInfo()', () => { + it('returns concatenated and formatted Uint8Array', () => { + const input = { + algorithmId : 'A128GCM', + partyUInfo : 'Alice', + partyVInfo : 'Bob', + suppPubInfo : 128, + suppPrivInfo : 'gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0' + }; + const output = 'AAAAB0ExMjhHQ00AAAAFQWxpY2UAAAADQm9iAAAAgAAAACtnSTBHQUlMQmR1N1Q1M2FrckZtTXlHY3NGM241ZE83TW13TkJIS1c1U1Yw'; + + // @ts-expect-error because computeOtherInfo() is a private method. + const otherInfo = ConcatKdf.computeOtherInfo(input); + + const expectedResult = Convert.base64Url(output).toUint8Array(); + expect(otherInfo).to.deep.equal(expectedResult); + }); + + it('matches RFC 7518 ECDH-ES key agreement computation example', async () => { + // Test vector 1. + const input = { + algorithmId : 'A128GCM', + partyUInfo : 'Alice', + partyVInfo : 'Bob', + suppPubInfo : 128 + }; + const output = 'AAAAB0ExMjhHQ00AAAAFQWxpY2UAAAADQm9iAAAAgA'; + + // @ts-expect-error because computeOtherInfo() is a private method. + const otherInfo = ConcatKdf.computeOtherInfo(input); + + const expectedResult = Convert.base64Url(output).toUint8Array(); + expect(otherInfo).to.deep.equal(expectedResult); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/ed25519.spec.ts b/packages/crypto/tests/crypto-primitives/ed25519.spec.ts new file mode 100644 index 000000000..e2c153390 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/ed25519.spec.ts @@ -0,0 +1,381 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { JwkParamsOkpPrivate, PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import ed25519Sign from '../fixtures/test-vectors/ed25519/sign.json' assert { type: 'json' }; +import ed25519Verify from '../fixtures/test-vectors/ed25519/verify.json' assert { type: 'json' }; +import ed25519ComputePublicKey from '../fixtures/test-vectors/ed25519/compute-public-key.json' assert { type: 'json' }; +import ed25519BytesToPublicKey from '../fixtures/test-vectors/ed25519/bytes-to-public-key.json' assert { type: 'json' }; +import ed25519PublicKeyToBytes from '../fixtures/test-vectors/ed25519/public-key-to-bytes.json' assert { type: 'json' }; +import ed25519BytesToPrivateKey from '../fixtures/test-vectors/ed25519/bytes-to-private-key.json' assert { type: 'json' }; +import ed25519PrivateKeyToBytes from '../fixtures/test-vectors/ed25519/private-key-to-bytes.json' assert { type: 'json' }; +import ed25519ConvertPublicKeyToX25519 from '../fixtures/test-vectors/ed25519/convert-public-key-to-x25519.json' assert { type: 'json' }; +import ed25519ConvertPrivateKeyToX25519 from '../fixtures/test-vectors/ed25519/convert-private-key-to-x25519.json' assert { type: 'json' }; + +import { Ed25519 } from '../../src/crypto-primitives/ed25519.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('Ed25519', () => { + let privateKey: PrivateKeyJwk; + let publicKey: PublicKeyJwk; + + before(async () => { + privateKey = await Ed25519.generateKey(); + publicKey = await Ed25519.computePublicKey({ privateKey }); + }); + + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb').toUint8Array(); + const privateKey = await Ed25519.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('crv', 'Ed25519'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('x'); + }); + + for (const vector of ed25519BytesToPrivateKey.vectors) { + it(vector.description, async () => { + const privateKey = await Ed25519.bytesToPrivateKey({ + privateKeyBytes: Convert.hex(vector.input.privateKeyBytes).toUint8Array() + }); + expect(privateKey).to.deep.equal(vector.output); + }); + } + }); + + describe('bytesToPublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKeyBytes = Convert.hex('3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c').toUint8Array(); + const publicKey = await Ed25519.bytesToPublicKey({ publicKeyBytes }); + + expect(publicKey).to.have.property('crv', 'Ed25519'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('kty', 'OKP'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.not.have.property('d'); + }); + + for (const vector of ed25519BytesToPublicKey.vectors) { + it(vector.description, async () => { + const publicKey = await Ed25519.bytesToPublicKey({ + publicKeyBytes: Convert.hex(vector.input.publicKeyBytes).toUint8Array() + }); + expect(publicKey).to.deep.equal(vector.output); + }); + } + }); + + describe('computePublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKey = await Ed25519.computePublicKey({ privateKey }); + + expect(publicKey).to.have.property('kty', 'OKP'); + expect(publicKey).to.have.property('crv', 'Ed25519'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.not.have.property('d'); + }); + + for (const vector of ed25519ComputePublicKey.vectors) { + it(vector.description, async () => { + const publicKey = await Ed25519.computePublicKey(vector.input as { privateKey: PrivateKeyJwk }); + expect(publicKey).to.deep.equal(vector.output); + }); + } + }); + + describe('convertPrivateKeyToX25519()', () => { + for (const vector of ed25519ConvertPrivateKeyToX25519.vectors) { + it(vector.description, async () => { + const x25519PrivateKey = await Ed25519.convertPrivateKeyToX25519( + vector.input as { privateKey: PrivateKeyJwk } + ); + expect(x25519PrivateKey).to.deep.equal(vector.output); + }); + } + }); + + describe('convertPublicKeyToX25519()', () => { + it('throws an error when provided an invalid Ed25519 public key', async () => { + const invalidEd25519PublicKeyBytes = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); + + const invalidEd25519PublicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : Convert.uint8Array(invalidEd25519PublicKeyBytes).toBase64Url() + }; + + await expect( + Ed25519.convertPublicKeyToX25519({ publicKey: invalidEd25519PublicKey }) + ).to.eventually.be.rejectedWith(Error, 'Invalid public key'); + }); + + it('throws an error when provided an Ed25519 private key', async () => { + const ed25519PrivateKey: PrivateKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + d : 'dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo', + x : '0KTOwPi1C6HpNuxWFUVKqX37J4ZPXxdgivLLsQVI8bM' + }; + + await expect( + Ed25519.convertPublicKeyToX25519({ publicKey: ed25519PrivateKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid OKP public key'); + }); + + for (const vector of ed25519ConvertPublicKeyToX25519.vectors) { + it(vector.description, async () => { + const x25519PrivateKey = await Ed25519.convertPublicKeyToX25519( + vector.input as { publicKey: PublicKeyJwk } + ); + expect(x25519PrivateKey).to.deep.equal(vector.output); + }); + } + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await Ed25519.generateKey(); + + expect(privateKey).to.have.property('crv', 'Ed25519'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('x'); + }); + + it('returns a 32-byte private key', async () => { + const privateKey = await Ed25519.generateKey() as JwkParamsOkpPrivate; + + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(32); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey: PrivateKeyJwk = { + crv : 'Ed25519', + d : 'TM0Imyj_ltqdtsNG7BFOD1uKMZ81q6Yk2oz27U-4pvs', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + kid : 'FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk' + }; + const privateKeyBytes = await Ed25519.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an Ed25519 public key', async () => { + const publicKey: PublicKeyJwk = { + crv : 'Ed25519', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + Ed25519.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid OKP private key'); + }); + + for (const vector of ed25519PrivateKeyToBytes.vectors) { + it(vector.description, async () => { + const privateKeyBytes = await Ed25519.privateKeyToBytes({ + privateKey: vector.input.privateKey as PrivateKeyJwk + }); + expect(privateKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('publicKeyToBytes()', () => { + it('returns a public key in JWK format', async () => { + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + kid : 'FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk' + }; + + const publicKeyBytes = await Ed25519.publicKeyToBytes({ publicKey }); + + expect(publicKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c').toUint8Array(); + expect(publicKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an Ed25519 private key', async () => { + const privateKey: PrivateKeyJwk = { + crv : 'Ed25519', + d : 'TM0Imyj_ltqdtsNG7BFOD1uKMZ81q6Yk2oz27U-4pvs', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + kid : 'FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk' + }; + + await expect( + Ed25519.publicKeyToBytes({ publicKey: privateKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid OKP public key'); + }); + + for (const vector of ed25519PublicKeyToBytes.vectors) { + it(vector.description, async () => { + const publicKeyBytes = await Ed25519.publicKeyToBytes({ + publicKey: vector.input.publicKey as PublicKeyJwk + }); + expect(publicKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('sign()', () => { + it('returns a 64-byte signature of type Uint8Array', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Ed25519.sign({ key: privateKey, data }); + expect(signature).to.be.instanceOf(Uint8Array); + expect(signature.byteLength).to.equal(64); + }); + + it('accepts input data as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let signature: Uint8Array; + + signature = await Ed25519.sign({ key: privateKey, data: data }); + expect(signature).to.be.instanceOf(Uint8Array); + }); + + for (const vector of ed25519Sign.vectors) { + it(vector.description, async () => { + const signature = await Ed25519.sign({ + data : Convert.hex(vector.input.data).toUint8Array(), + key : vector.input.key as PrivateKeyJwk + }); + + const signatureHex = Convert.uint8Array(signature).toHex(); + expect(signatureHex).to.deep.equal(vector.output); + }); + } + }); + + describe('validatePublicKey()', () => { + it('returns true for valid public keys', async () => { + const key = Convert.hex('a12c2beb77265f2aac953b5009349d94155a03ada416aad451319480e983ca4c').toUint8Array(); + // @ts-expect-error because validatePublicKey() is a private method. + const isValid = await Ed25519.validatePublicKey({ key }); + expect(isValid).to.be.true; + }); + + it('returns false for invalid public keys', async () => { + const key = Convert.hex('02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f').toUint8Array(); + // @ts-expect-error because validatePublicKey() is a private method. + const isValid = await Ed25519.validatePublicKey({ key }); + expect(isValid).to.be.false; + }); + + it('returns false if a private key is given', async () => { + const key = Convert.hex('0a23a20072891237aa0864b5765139514908787878cd77135a0059881d313f00').toUint8Array(); + // @ts-expect-error because validatePublicKey() is a private method. + const isValid = await Ed25519.validatePublicKey({ key }); + expect(isValid).to.be.false; + }); + }); + + describe('verify()', () => { + it('returns a boolean result', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Ed25519.sign({ key: privateKey, data }); + + const isValid = await Ed25519.verify({ key: publicKey, signature, data }); + expect(isValid).to.exist; + expect(isValid).to.be.a('boolean'); + }); + + it('accepts input data as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const signature = await Ed25519.sign({ key: privateKey, data }); + + const isValid = await Ed25519.verify({ key: publicKey, signature, data }); + expect(isValid).to.be.true; + }); + + it('returns false if the signed data was mutated', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let isValid: boolean; + + // Generate signature using the private key. + const signature = await Ed25519.sign({ key: privateKey, data }); + + // Verification should return true with the data used to generate the signature. + isValid = await Ed25519.verify({ key: publicKey, signature, data }); + expect(isValid).to.be.true; + + // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. + const mutatedData = new Uint8Array(data); + mutatedData[0] ^= 1 << 0; + + // Verification should return false if the given data does not match the data used to generate the signature. + isValid = await Ed25519.verify({ key: publicKey, signature, data: mutatedData }); + expect(isValid).to.be.false; + }); + + it('returns false if the signature was mutated', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let isValid: boolean; + + // Generate a new private key and get its public key. + privateKey = await Ed25519.generateKey(); + publicKey = await Ed25519.computePublicKey({ privateKey }); + + // Generate signature using the private key. + const signature = await Ed25519.sign({ key: privateKey, data }); + + // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. + const mutatedSignature = new Uint8Array(signature); + mutatedSignature[0] ^= 1 << 0; + + // Verification should return false if the signature was modified. + isValid = await Ed25519.verify({ key: publicKey, signature: signature, data: mutatedSignature }); + expect(isValid).to.be.false; + }); + + it('returns false with a signature generated using a different private key', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const privateKeyA = await Ed25519.generateKey(); + const publicKeyA = await Ed25519.computePublicKey({ privateKey: privateKeyA }); + const privateKeyB = await Ed25519.generateKey(); + let isValid: boolean; + + // Generate a signature using private key B. + const signatureB = await Ed25519.sign({ key: privateKeyB, data }); + + // Verification should return false with the public key from private key A. + isValid = await Ed25519.verify({ key: publicKeyA, signature: signatureB, data }); + expect(isValid).to.be.false; + }); + + for (const vector of ed25519Verify.vectors) { + it(vector.description, async () => { + const isValid = await Ed25519.verify({ + data : Convert.hex(vector.input.data).toUint8Array(), + key : vector.input.key as PublicKeyJwk, + signature : Convert.hex(vector.input.signature).toUint8Array() + }); + expect(isValid).to.equal(vector.output); + }); + } + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/pbkdf2.spec.ts b/packages/crypto/tests/crypto-primitives/pbkdf2.spec.ts new file mode 100644 index 000000000..65119a223 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/pbkdf2.spec.ts @@ -0,0 +1,133 @@ +import sinon from 'sinon'; +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import { Pbkdf2 } from '../../src/crypto-primitives/pbkdf2.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('Pbkdf2', () => { + const password = Convert.string('password').toUint8Array(); + const salt = Convert.string('salt').toUint8Array(); + const iterations = 1; + const length = 256; // 32 bytes + + describe('deriveKey', () => { + it('successfully derives a key using WebCrypto, if available', async () => { + const subtleDeriveBitsSpy = sinon.spy(crypto.subtle, 'deriveBits'); + + const derivedKey = await Pbkdf2.deriveKey({ hash: 'SHA-256', password, salt, iterations, length }); + + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(length / 8); + expect(subtleDeriveBitsSpy.called).to.be.true; + + subtleDeriveBitsSpy.restore(); + }); + + it('successfully derives a key using node:crypto when WebCrypto is not supported', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + // Ensure that WebCrypto is not available for this test. + sinon.stub(crypto, 'subtle').value(null); + + // @ts-expect-error because we're spying on a private method. + const nodeCryptoDeriveKeySpy = sinon.spy(Pbkdf2, 'deriveKeyWithNodeCrypto'); + + const derivedKey = await Pbkdf2.deriveKey({ hash: 'SHA-256', password, salt, iterations, length }); + + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(length / 8); + expect(nodeCryptoDeriveKeySpy.called).to.be.true; + + nodeCryptoDeriveKeySpy.restore(); + sinon.restore(); + }); + + it('derives the same value with node:crypto and WebCrypto', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + const options = { hash: 'SHA-256', password, salt, iterations, length }; + + // @ts-expect-error because we're testing a private method. + const webCryptoDerivedKey = await Pbkdf2.deriveKeyWithNodeCrypto(options); + // @ts-expect-error because we're testing a private method. + const nodeCryptoDerivedKey = await Pbkdf2.deriveKeyWithWebCrypto(options); + + expect(webCryptoDerivedKey).to.deep.equal(nodeCryptoDerivedKey); + }); + + const hashFunctions: ('SHA-256' | 'SHA-384' | 'SHA-512')[] = ['SHA-256', 'SHA-384', 'SHA-512']; + hashFunctions.forEach(hash => { + it(`handles ${hash} hash function`, async () => { + const options = { hash, password, salt, iterations, length }; + + const derivedKey = await Pbkdf2.deriveKey(options); + expect(derivedKey).to.be.instanceOf(Uint8Array); + expect(derivedKey.byteLength).to.equal(length / 8); + }); + }); + + it('throws an error when an invalid hash function is used with WebCrypto', async () => { + const options = { + hash: 'SHA-2' as const, password, salt, iterations, length + }; + + // @ts-expect-error for testing purposes + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); + }); + + it('throws an error when an invalid hash function is used with node:crypto', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + // Ensure that WebCrypto is not available for this test. + sinon.stub(crypto, 'subtle').value(null); + + const options = { + hash: 'SHA-2' as const, password, salt, iterations, length + }; + + // @ts-expect-error for testing purposes + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); + + sinon.restore(); + }); + + it('throws an error when iterations count is not a positive number with WebCrypto', async () => { + const options = { + hash : 'SHA-256' as const, password, salt, + iterations : -1, length + }; + + // Every browser throws a different error message so a specific message cannot be checked. + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error); + }); + + it('throws an error when iterations count is not a positive number with node:crypto', async function () { + // Skip test in web browsers since node:crypto is not available. + if (typeof window !== 'undefined') this.skip(); + + // Ensure that WebCrypto is not available for this test. + sinon.stub(crypto, 'subtle').value(null); + + const options = { + hash : 'SHA-256' as const, password, salt, + iterations : -1, length + }; + + await expect(Pbkdf2.deriveKey(options)).to.eventually.be.rejectedWith(Error, 'out of range'); + + sinon.restore(); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/secp256k1.spec.ts b/packages/crypto/tests/crypto-primitives/secp256k1.spec.ts new file mode 100644 index 000000000..c2a97427e --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/secp256k1.spec.ts @@ -0,0 +1,442 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { JwkParamsEcPrivate, PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import secp256k1GetCurvePoints from '../fixtures/test-vectors/secp256k1/get-curve-points.json' assert { type: 'json' }; +import secp256k1BytesToPublicKey from '../fixtures/test-vectors/secp256k1/bytes-to-public-key.json' assert { type: 'json' }; +import secp256k1PublicKeyToBytes from '../fixtures/test-vectors/secp256k1/public-key-to-bytes.json' assert { type: 'json' }; +import secp256k1ValidatePublicKey from '../fixtures/test-vectors/secp256k1/validate-public-key.json' assert { type: 'json' }; +import secp256k1BytesToPrivateKey from '../fixtures/test-vectors/secp256k1/bytes-to-private-key.json' assert { type: 'json' }; +import secp256k1PrivateKeyToBytes from '../fixtures/test-vectors/secp256k1/private-key-to-bytes.json' assert { type: 'json' }; +import secp256k1ValidatePrivateKey from '../fixtures/test-vectors/secp256k1/validate-private-key.json' assert { type: 'json' }; + +import { Secp256k1 } from '../../src/crypto-primitives/secp256k1.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('Secp256k1', () => { + let privateKey: PrivateKeyJwk; + let publicKey: PublicKeyJwk; + + before(async () => { + privateKey = await Secp256k1.generateKey(); + publicKey = await Secp256k1.computePublicKey({ privateKey }); + }); + + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('740ec69810de9ad1b8f298f1d2c0e6a52dd1e958dc2afc85764bec169c222e88').toUint8Array(); + const privateKey = await Secp256k1.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('crv', 'secp256k1'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'EC'); + expect(privateKey).to.have.property('x'); + expect(privateKey).to.have.property('y'); + }); + + for (const vector of secp256k1BytesToPrivateKey.vectors) { + it(vector.description, async () => { + const privateKey = await Secp256k1.bytesToPrivateKey({ + privateKeyBytes: Convert.hex(vector.input.privateKeyBytes).toUint8Array() + }); + + expect(privateKey).to.deep.equal(vector.output); + }); + } + }); + + describe('bytesToPublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKeyBytes = Convert.hex('043752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553').toUint8Array(); + const publicKey = await Secp256k1.bytesToPublicKey({ publicKeyBytes }); + + expect(publicKey).to.have.property('crv', 'secp256k1'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.have.property('y'); + expect(publicKey).to.not.have.property('d'); + }); + + for (const vector of secp256k1BytesToPublicKey.vectors) { + it(vector.description, async () => { + const publicKey = await Secp256k1.bytesToPublicKey({ + publicKeyBytes: Convert.hex(vector.input.publicKeyBytes).toUint8Array() + }); + expect(publicKey).to.deep.equal(vector.output); + }); + } + }); + + describe('computePublicKey()', () => { + it('returns a public key in JWK format', async () => { + publicKey = await Secp256k1.computePublicKey({ privateKey }); + + expect(publicKey).to.have.property('crv', 'secp256k1'); + expect(publicKey).to.not.have.property('d'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('kty', 'EC'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.have.property('y'); + }); + }); + + describe('compressPublicKey()', () => { + it('converts an uncompressed public key to compressed format', async () => { + const compressedPublicKeyBytes = Convert.hex('026bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce214').toUint8Array(); + const uncompressedPublicKeyBytes = Convert.hex('046bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce21465062296011dd076ae4e8ce5163ccf69d01496d3147656dcc96645b95211f3c6').toUint8Array(); + + const output = await Secp256k1.compressPublicKey({ + publicKeyBytes: uncompressedPublicKeyBytes + }); + + // Confirm the length of the resulting public key is 33 bytes + expect(output.byteLength).to.equal(33); + + // Confirm the output matches the expected compressed public key. + expect(output).to.deep.equal(compressedPublicKeyBytes); + }); + + it('throws an error for an invalid uncompressed public key', async () => { + // Invalid uncompressed public key. + const invalidPublicKey = Convert.hex('dfebc16793a5737ac51f606a43524df8373c063e41d5a99b2f1530afd987284bd1c7cde1658a9a756e71f44a97b4783ea9dee5ccb7f1447eb4836d8de9bd4f81fd').toUint8Array(); + + try { + await Secp256k1.compressPublicKey({ + publicKeyBytes: invalidPublicKey, + }); + expect.fail('Expected method to throw an error.'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Point of length 65 was invalid'); + } + }); + }); + + describe('decompressPublicKey()', () => { + it('converts a compressed public key to an uncompressed format', async () => { + const compressedPublicKeyBytes = Convert.hex('026bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce214').toUint8Array(); + const uncompressedPublicKeyBytes = Convert.hex('046bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce21465062296011dd076ae4e8ce5163ccf69d01496d3147656dcc96645b95211f3c6').toUint8Array(); + + const output = await Secp256k1.decompressPublicKey({ + publicKeyBytes: compressedPublicKeyBytes + }); + + // Confirm the length of the resulting public key is 65 bytes + expect(output.byteLength).to.equal(65); + + // Confirm the output matches the expected uncompressed public key. + expect(output).to.deep.equal(uncompressedPublicKeyBytes); + }); + + it('throws an error for an invalid compressed public key', async () => { + // Invalid compressed public key. + const invalidPublicKey = Convert.hex('fef0b998921eafb58f49efdeb0adc47123aa28a4042924236f08274d50c72fe7b0').toUint8Array(); + + try { + await Secp256k1.decompressPublicKey({ + publicKeyBytes: invalidPublicKey, + }); + expect.fail('Expected method to throw an error.'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Point of length 33 was invalid'); + } + }); + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await Secp256k1.generateKey(); + + expect(privateKey).to.have.property('crv', 'secp256k1'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'EC'); + expect(privateKey).to.have.property('x'); + expect(privateKey).to.have.property('y'); + }); + + it('returns a 32-byte private key', async () => { + const privateKey = await Secp256k1.generateKey() as JwkParamsEcPrivate; + + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(32); + }); + }); + + describe('getCurvePoints()', () => { + for (const vector of secp256k1GetCurvePoints.vectors) { + it(vector.description, async () => { + const key = Convert.hex(vector.input.key).toUint8Array(); + // @ts-expect-error because getCurvePoints() is a private method. + const points = await Secp256k1.getCurvePoints({ key }); + expect(points.x).to.deep.equal(Convert.hex(vector.output.x).toUint8Array()); + expect(points.y).to.deep.equal(Convert.hex(vector.output.y).toUint8Array()); + }); + } + + it('throws error with invalid input key length', async () => { + await expect( + // @ts-expect-error because getCurvePoints() is a private method. + Secp256k1.getCurvePoints({ key: new Uint8Array(16) }) + ).to.eventually.be.rejectedWith(Error, 'Point of length 16 was invalid. Expected 33 compressed bytes or 65 uncompressed bytes'); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey: PrivateKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + d : 'dA7GmBDemtG48pjx0sDmpS3R6VjcKvyFdkvsFpwiLog', + x : 'N1KVEnQCMpbIp0sP_kL4L_S01LukMmR3QicD92H1klg', + y : 'wmp0ZbmnesDD8c7bE5xCiwsfu1UWhntSdjbzKG9wVVM', + kid : 'iwwOeCqgvREo5xGeBS-obWW9ZGjv0o1M65gUYN6SYh4' + }; + const privateKeyBytes = await Secp256k1.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('740ec69810de9ad1b8f298f1d2c0e6a52dd1e958dc2afc85764bec169c222e88').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided a secp256k1 public key', async () => { + const publicKey: PublicKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : 'N1KVEnQCMpbIp0sP_kL4L_S01LukMmR3QicD92H1klg', + y : 'wmp0ZbmnesDD8c7bE5xCiwsfu1UWhntSdjbzKG9wVVM' + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + Secp256k1.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid EC private key'); + }); + + for (const vector of secp256k1PrivateKeyToBytes.vectors) { + it(vector.description, async () => { + const privateKeyBytes = await Secp256k1.privateKeyToBytes({ + privateKey: vector.input.privateKey as PrivateKeyJwk + }); + expect(privateKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('publicKeyToBytes()', () => { + it('returns a public key in JWK format', async () => { + const publicKey: PublicKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + x : 'N1KVEnQCMpbIp0sP_kL4L_S01LukMmR3QicD92H1klg', + y : 'wmp0ZbmnesDD8c7bE5xCiwsfu1UWhntSdjbzKG9wVVM', + kid : 'iwwOeCqgvREo5xGeBS-obWW9ZGjv0o1M65gUYN6SYh4' + }; + + const publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey }); + + expect(publicKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('043752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553').toUint8Array(); + expect(publicKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an Ed25519 private key', async () => { + const privateKey: PrivateKeyJwk = { + kty : 'EC', + crv : 'secp256k1', + d : 'dA7GmBDemtG48pjx0sDmpS3R6VjcKvyFdkvsFpwiLog', + x : 'N1KVEnQCMpbIp0sP_kL4L_S01LukMmR3QicD92H1klg', + y : 'wmp0ZbmnesDD8c7bE5xCiwsfu1UWhntSdjbzKG9wVVM', + kid : 'iwwOeCqgvREo5xGeBS-obWW9ZGjv0o1M65gUYN6SYh4' + }; + + await expect( + Secp256k1.publicKeyToBytes({ publicKey: privateKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid EC public key'); + }); + + for (const vector of secp256k1PublicKeyToBytes.vectors) { + it(vector.description, async () => { + const publicKeyBytes = await Secp256k1.publicKeyToBytes({ + publicKey: vector.input.publicKey as PublicKeyJwk + }); + expect(publicKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('sharedSecret()', () => { + let ownPrivateKey: PrivateKeyJwk; + let ownPublicKey: PublicKeyJwk; + let otherPartyPrivateKey: PrivateKeyJwk; + let otherPartyPublicKey: PublicKeyJwk; + + beforeEach(async () => { + ownPrivateKey = privateKey; + ownPublicKey = publicKey; + + otherPartyPrivateKey = await Secp256k1.generateKey(); + otherPartyPublicKey = await Secp256k1.computePublicKey({ privateKey: otherPartyPrivateKey }); + }); + + it('generates a 32-byte shared secret', async () => { + const sharedSecret = await Secp256k1.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : otherPartyPublicKey + }); + expect(sharedSecret).to.be.instanceOf(Uint8Array); + expect(sharedSecret.byteLength).to.equal(32); + }); + + it('is commutative', async () => { + const sharedSecretOwnOther = await Secp256k1.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : otherPartyPublicKey + }); + + const sharedSecretOtherOwn = await Secp256k1.sharedSecret({ + privateKeyA : otherPartyPrivateKey, + publicKeyB : ownPublicKey + }); + + expect(sharedSecretOwnOther).to.deep.equal(sharedSecretOtherOwn); + }); + + it('throws an error if the public/private keys from the same key pair are specified', async () => { + await expect( + Secp256k1.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : ownPublicKey + }) + ).to.eventually.be.rejectedWith(Error, 'shared secret cannot be computed from a single key pair'); + }); + }); + + describe('sign()', () => { + it('returns a 64-byte signature of type Uint8Array', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Secp256k1.sign({ key: privateKey, data }); + expect(signature).to.be.instanceOf(Uint8Array); + expect(signature.byteLength).to.equal(64); + }); + + it('accepts input data as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const key = privateKey; + let signature: Uint8Array; + + signature = await Secp256k1.sign({ key, data }); + expect(signature).to.be.instanceOf(Uint8Array); + }); + }); + + describe('validatePrivateKey()', () => { + for (const vector of secp256k1ValidatePrivateKey.vectors) { + it(vector.description, async () => { + const key = Convert.hex(vector.input.key).toUint8Array(); + // @ts-expect-error because validatePrivateKey() is a private method. + const isValid = await Secp256k1.validatePrivateKey({ key }); + expect(isValid).to.equal(vector.output); + }); + } + }); + + describe('validatePublicKey()', () => { + for (const vector of secp256k1ValidatePublicKey.vectors) { + it(vector.description, async () => { + const key = Convert.hex(vector.input.key).toUint8Array(); + // @ts-expect-error because validatePublicKey() is a private method. + const isValid = await Secp256k1.validatePublicKey({ key }); + expect(isValid).to.equal(vector.output); + }); + } + }); + + describe('verify()', () => { + it('returns a boolean result', async () => { + const data = new Uint8Array([51, 52, 53]); + const signature = await Secp256k1.sign({ key: privateKey, data }); + + const isValid = await Secp256k1.verify({ key: publicKey, signature, data }); + expect(isValid).to.exist; + expect(isValid).to.be.true; + }); + + it('accepts input data as Uint8Array', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let isValid: boolean; + let signature: Uint8Array; + + // TypedArray - Uint8Array + signature = await Secp256k1.sign({ key: privateKey, data }); + isValid = await Secp256k1.verify({ key: publicKey, signature, data }); + expect(isValid).to.be.true; + }); + + it('returns false if the signed data was mutated', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let isValid: boolean; + + // Generate signature using the private key. + const signature = await Secp256k1.sign({ key: privateKey, data }); + + // Verification should return true with the data used to generate the signature. + isValid = await Secp256k1.verify({ key: publicKey, signature, data }); + expect(isValid).to.be.true; + + // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. + const mutatedData = new Uint8Array(data); + mutatedData[0] ^= 1 << 0; + + // Verification should return false if the given data does not match the data used to generate the signature. + isValid = await Secp256k1.verify({ key: publicKey, signature, data: mutatedData }); + expect(isValid).to.be.false; + }); + + it('returns false if the signature was mutated', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + let isValid: boolean; + + // Generate signature using the private key. + const signature = await Secp256k1.sign({ key: privateKey, data }); + + // Verification should return true with the data used to generate the signature. + isValid = await Secp256k1.verify({ key: publicKey, signature, data }); + expect(isValid).to.be.true; + + // Make a copy and flip the least significant bit (the rightmost bit) in the first byte of the array. + const mutatedSignature = new Uint8Array(signature); + mutatedSignature[0] ^= 1 << 0; + + // Verification should return false if the signature was modified. + isValid = await Secp256k1.verify({ key: publicKey, signature: signature, data: mutatedSignature }); + expect(isValid).to.be.false; + }); + + it('returns false with a signature generated using a different private key', async () => { + const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const publicKeyA = publicKey; + const privateKeyB = await Secp256k1.generateKey(); + let isValid: boolean; + + // Generate a signature using private key B. + const signatureB = await Secp256k1.sign({ key: privateKeyB, data }); + + // Verification should return false with public key A. + isValid = await Secp256k1.verify({ key: publicKeyA, signature: signatureB, data }); + expect(isValid).to.be.false; + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/x25519.spec.ts b/packages/crypto/tests/crypto-primitives/x25519.spec.ts new file mode 100644 index 000000000..8a85d0d8b --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/x25519.spec.ts @@ -0,0 +1,241 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { JwkParamsOkpPrivate, PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import x25519BytesToPublicKey from '../fixtures/test-vectors/x25519/bytes-to-public-key.json' assert { type: 'json' }; +import x25519BytesToPrivateKey from '../fixtures/test-vectors/x25519/bytes-to-private-key.json' assert { type: 'json' }; +import x25519PrivateKeyToBytes from '../fixtures/test-vectors/x25519/private-key-to-bytes.json' assert { type: 'json' }; +import x25519PublicKeyToBytes from '../fixtures/test-vectors/x25519/public-key-to-bytes.json' assert { type: 'json' }; + +import { X25519 } from '../../src/crypto-primitives/x25519.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('X25519', () => { + let privateKey: PrivateKeyJwk; + let publicKey: PublicKeyJwk; + + before(async () => { + privateKey = await X25519.generateKey(); + publicKey = await X25519.computePublicKey({ privateKey }); + }); + + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475').toUint8Array(); + const privateKey = await X25519.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('crv', 'X25519'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('x'); + }); + + for (const vector of x25519BytesToPrivateKey.vectors) { + it(vector.description, async () => { + const privateKey = await X25519.bytesToPrivateKey({ + privateKeyBytes: Convert.hex(vector.input.privateKeyBytes).toUint8Array() + }); + + expect(privateKey).to.deep.equal(vector.output); + }); + } + }); + + describe('bytesToPublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKeyBytes = Convert.hex('504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829').toUint8Array(); + const publicKey = await X25519.bytesToPublicKey({ publicKeyBytes }); + + expect(publicKey).to.have.property('crv', 'X25519'); + expect(publicKey).to.have.property('kid'); + expect(publicKey).to.have.property('kty', 'OKP'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.not.have.property('d'); + }); + + for (const vector of x25519BytesToPublicKey.vectors) { + it(vector.description, async () => { + const publicKey = await X25519.bytesToPublicKey({ + publicKeyBytes: Convert.hex(vector.input.publicKeyBytes).toUint8Array() + }); + + expect(publicKey).to.deep.equal(vector.output); + }); + } + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await X25519.generateKey(); + + expect(privateKey).to.have.property('crv', 'X25519'); + expect(privateKey).to.have.property('d'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'OKP'); + expect(privateKey).to.have.property('x'); + }); + + it('returns a 32-byte private key', async () => { + const privateKey = await X25519.generateKey() as JwkParamsOkpPrivate; + + const privateKeyBytes = Convert.base64Url(privateKey.d).toUint8Array(); + expect(privateKeyBytes.byteLength).to.equal(32); + }); + }); + + describe('computePublicKey()', () => { + it('returns a public key in JWK format', async () => { + const publicKey = await X25519.computePublicKey({ privateKey }); + + expect(publicKey).to.have.property('kty', 'OKP'); + expect(publicKey).to.have.property('crv', 'X25519'); + expect(publicKey).to.have.property('x'); + expect(publicKey).to.not.have.property('d'); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey: PrivateKeyJwk = { + kty : 'OKP', + crv : 'X25519', + d : 'jxSSX_aM49m6E4MaSd-hcizIM33rXzLltuev9oBw1V8', + x : 'U2kX2FckTAoTAjMBUadwOpftdXk-Kx8pZMeyG3QZsy8', + kid : 'PPgSyqA-j9sc9vmsvpSCpy2uLg_CUfGoKHhPzQ5Gkog' + }; + const privateKeyBytes = await X25519.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('8f14925ff68ce3d9ba13831a49dfa1722cc8337deb5f32e5b6e7aff68070d55f').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an X25519 public key', async () => { + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'X25519', + x : 'U2kX2FckTAoTAjMBUadwOpftdXk-Kx8pZMeyG3QZsy8', + kid : 'PPgSyqA-j9sc9vmsvpSCpy2uLg_CUfGoKHhPzQ5Gkog' + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + X25519.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid OKP private key'); + }); + + for (const vector of x25519PrivateKeyToBytes.vectors) { + it(vector.description, async () => { + const privateKeyBytes = await X25519.privateKeyToBytes({ + privateKey: vector.input.privateKey as PrivateKeyJwk + }); + expect(privateKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('publicKeyToBytes()', () => { + it('returns a public key in JWK format', async () => { + const publicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'X25519', + x : 'U2kX2FckTAoTAjMBUadwOpftdXk-Kx8pZMeyG3QZsy8', + kid : 'PPgSyqA-j9sc9vmsvpSCpy2uLg_CUfGoKHhPzQ5Gkog' + }; + + const publicKeyBytes = await X25519.publicKeyToBytes({ publicKey }); + + expect(publicKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('536917d857244c0a1302330151a7703a97ed75793e2b1f2964c7b21b7419b32f').toUint8Array(); + expect(publicKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an X25519 private key', async () => { + const privateKey: PrivateKeyJwk = { + kty : 'OKP', + crv : 'X25519', + d : 'jxSSX_aM49m6E4MaSd-hcizIM33rXzLltuev9oBw1V8', + x : 'U2kX2FckTAoTAjMBUadwOpftdXk-Kx8pZMeyG3QZsy8', + kid : 'PPgSyqA-j9sc9vmsvpSCpy2uLg_CUfGoKHhPzQ5Gkog' + }; + + await expect( + X25519.publicKeyToBytes({ publicKey: privateKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid OKP public key'); + }); + + for (const vector of x25519PublicKeyToBytes.vectors) { + it(vector.description, async () => { + const publicKeyBytes = await X25519.publicKeyToBytes({ + publicKey: vector.input.publicKey as PublicKeyJwk + }); + expect(publicKeyBytes).to.deep.equal(Convert.hex(vector.output).toUint8Array()); + }); + } + }); + + describe('sharedSecret()', () => { + let ownPrivateKey: PrivateKeyJwk; + let ownPublicKey: PublicKeyJwk; + let otherPartyPrivateKey: PrivateKeyJwk; + let otherPartyPublicKey: PublicKeyJwk; + + before(async () => { + ownPrivateKey = privateKey; + ownPublicKey = publicKey; + otherPartyPrivateKey = await X25519.generateKey(); + otherPartyPublicKey = await X25519.computePublicKey({ privateKey: otherPartyPrivateKey }); + }); + + it('generates a 32-byte compressed secret', async () => { + const sharedSecret = await X25519.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : otherPartyPublicKey + }); + expect(sharedSecret).to.be.instanceOf(Uint8Array); + expect(sharedSecret.byteLength).to.equal(32); + }); + + it('is commutative', async () => { + const sharedSecretOwnOther = await X25519.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : otherPartyPublicKey + }); + + const sharedSecretOtherOwn = await X25519.sharedSecret({ + privateKeyA : otherPartyPrivateKey, + publicKeyB : ownPublicKey + }); + + expect(sharedSecretOwnOther).to.deep.equal(sharedSecretOtherOwn); + }); + + it('throws an error if the public/private keys from the same key pair are specified', async () => { + await expect( + X25519.sharedSecret({ + privateKeyA : ownPrivateKey, + publicKeyB : ownPublicKey + }) + ).to.eventually.be.rejectedWith(Error, 'shared secret cannot be computed from a single key pair'); + }); + }); + + describe('validatePublicKey()', () => { + it('throws a not implemented error', async () => { + await expect( + // @ts-expect-error because validatePublicKey is a private method. + X25519.validatePublicKey({ key: new Uint8Array(32) }) + ).to.eventually.be.rejectedWith(Error, 'Not implemented'); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/xchacha20-poly1305.spec.ts b/packages/crypto/tests/crypto-primitives/xchacha20-poly1305.spec.ts new file mode 100644 index 000000000..16b6d7810 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/xchacha20-poly1305.spec.ts @@ -0,0 +1,190 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import { XChaCha20Poly1305 } from '../../src/crypto-primitives/xchacha20-poly1305.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('XChaCha20Poly1305', () => { + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('ffbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + + it('returns the expected JWK given byte array input', async () => { + const privateKeyBytes = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + + const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + + const expectedOutput: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + expect(privateKey).to.deep.equal(expectedOutput); + }); + }); + + describe('decrypt()', () => { + it('returns Uint8Array plaintext with length matching input', async () => { + const plaintext = await XChaCha20Poly1305.decrypt({ + data : Convert.hex('789e9689e5208d7fd9e1').toUint8Array(), + key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), + nonce : new Uint8Array(24), + tag : Convert.hex('09701fb9f36ab77a0f136ca539229a34').toUint8Array() + }); + expect(plaintext).to.be.an('Uint8Array'); + expect(plaintext.byteLength).to.equal(10); + }); + + it('passes test vectors', async () => { + const privateKeyBytes = Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(); + const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + + const input = { + data : Convert.hex('80246ca517c0fb5860c19090a7e7a2b030dde4882520102cbc64fad937916596ca9d').toUint8Array(), + key : privateKey, + nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array(), + tag : Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array() + }; + const output = Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(); + + const plaintext = await XChaCha20Poly1305.decrypt({ + data : input.data, + key : input.key, + nonce : input.nonce, + tag : input.tag + }); + + expect(plaintext).to.deep.equal(output); + }); + + it('throws an error if an invalid tag is given', async () => { + await expect( + XChaCha20Poly1305.decrypt({ + data : new Uint8Array(10), + key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), + nonce : new Uint8Array(24), + tag : new Uint8Array(16) + }) + ).to.eventually.be.rejectedWith(Error, 'invalid tag'); + }); + }); + + describe('encrypt()', () => { + it('returns Uint8Array ciphertext and tag', async () => { + const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + data : new Uint8Array(10), + key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), + nonce : new Uint8Array(24) + }); + expect(ciphertext).to.be.an('Uint8Array'); + expect(ciphertext.byteLength).to.equal(10); + expect(tag).to.be.an('Uint8Array'); + expect(tag.byteLength).to.equal(16); + }); + + it('accepts additional authenticated data', async () => { + const { ciphertext: ciphertextAad, tag: tagAad } = await XChaCha20Poly1305.encrypt({ + additionalData : new Uint8Array(64), + data : new Uint8Array(10), + key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), + nonce : new Uint8Array(24) + }); + + const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + data : new Uint8Array(10), + key : await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes: new Uint8Array(32) }), + nonce : new Uint8Array(24) + }); + + expect(ciphertextAad.byteLength).to.equal(10); + expect(ciphertext.byteLength).to.equal(10); + expect(ciphertextAad).to.deep.equal(ciphertext); + expect(tagAad).to.not.deep.equal(tag); + }); + + it('passes test vectors', async () => { + const privateKeyBytes = Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(); + const privateKey = await XChaCha20Poly1305.bytesToPrivateKey({ privateKeyBytes }); + + const input = { + data : Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(), + key : privateKey, + nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array() + }; + const output = { + ciphertext : Convert.hex('80246ca517c0fb5860c19090a7e7a2b030dde4882520102cbc64fad937916596ca9d').toUint8Array(), + tag : Convert.hex('9e10a121d990e6a290f6b534516aa32f').toUint8Array() + }; + + const { ciphertext, tag } = await XChaCha20Poly1305.encrypt({ + data : input.data, + key : input.key, + nonce : input.nonce + }); + + expect(ciphertext).to.deep.equal(output.ciphertext); + expect(tag).to.deep.equal(output.tag); + }); + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await XChaCha20Poly1305.generateKey(); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey = await XChaCha20Poly1305.generateKey(); + const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + }); + + it('returns the expected byte array for JWK input', async () => { + const privateKey: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + const privateKeyBytes = await XChaCha20Poly1305.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an asymmetric public key', async () => { + const publicKey: PublicKeyJwk = { + crv : 'Ed25519', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + XChaCha20Poly1305.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid oct private key'); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/crypto-primitives/xchacha20.spec.ts b/packages/crypto/tests/crypto-primitives/xchacha20.spec.ts new file mode 100644 index 000000000..c9cc85253 --- /dev/null +++ b/packages/crypto/tests/crypto-primitives/xchacha20.spec.ts @@ -0,0 +1,153 @@ +import chai, { expect } from 'chai'; +import { Convert } from '@web5/common'; +import chaiAsPromised from 'chai-as-promised'; + +import type { PrivateKeyJwk, PublicKeyJwk } from '../../src/jose.js'; + +import { XChaCha20 } from '../../src/crypto-primitives/xchacha20.js'; + +chai.use(chaiAsPromised); + +// NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage +// Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +describe('XChaCha20', () => { + describe('bytesToPrivateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKeyBytes = Convert.hex('ffbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + const privateKey = await XChaCha20.bytesToPrivateKey({ privateKeyBytes }); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + + it('returns the expected JWK given byte array input', async () => { + const privateKeyBytes = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + const privateKey = await XChaCha20.bytesToPrivateKey({ privateKeyBytes }); + + const expectedOutput: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + expect(privateKey).to.deep.equal(expectedOutput); + }); + }); + + describe('decrypt()', () => { + it('returns Uint8Array plaintext with length matching input', async () => { + const privateKey = await XChaCha20.generateKey(); + + const plaintext = await XChaCha20.decrypt({ + data : new Uint8Array(10), + key : privateKey, + nonce : new Uint8Array(24) + }); + expect(plaintext).to.be.an('Uint8Array'); + expect(plaintext.byteLength).to.equal(10); + }); + + it('passes test vectors', async () => { + const privateKeyBytes = Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(); + const privateKey = await XChaCha20.bytesToPrivateKey({ privateKeyBytes }); + + const input = { + data : Convert.hex('879b10a139674fe65087f59577ee2c1ab54655d900697fd02d953f53ddcc1ae476e8').toUint8Array(), + key : privateKey, + nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array() + }; + const output = Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(); + + const ciphertext = await XChaCha20.decrypt({ + data : input.data, + key : input.key, + nonce : input.nonce + }); + + expect(ciphertext).to.deep.equal(output); + }); + }); + + describe('encrypt()', () => { + it('returns Uint8Array ciphertext with length matching input', async () => { + const privateKey = await XChaCha20.generateKey(); + + const ciphertext = await XChaCha20.encrypt({ + data : new Uint8Array(10), + key : privateKey, + nonce : new Uint8Array(24) + }); + expect(ciphertext).to.be.an('Uint8Array'); + expect(ciphertext.byteLength).to.equal(10); + }); + + it('passes test vectors', async () => { + const privateKeyBytes = Convert.hex('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4').toUint8Array(); + const privateKey = await XChaCha20.bytesToPrivateKey({ privateKeyBytes }); + + const input = { + data : Convert.string(`Are You There Bob? It's Me, Alice.`).toUint8Array(), + key : privateKey, + nonce : Convert.hex('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419').toUint8Array() + }; + const output = Convert.hex('879b10a139674fe65087f59577ee2c1ab54655d900697fd02d953f53ddcc1ae476e8').toUint8Array(); + + const ciphertext = await XChaCha20.encrypt({ + data : input.data, + key : input.key, + nonce : input.nonce + }); + + expect(ciphertext).to.deep.equal(output); + }); + }); + + describe('generateKey()', () => { + it('returns a private key in JWK format', async () => { + const privateKey = await XChaCha20.generateKey(); + + expect(privateKey).to.have.property('k'); + expect(privateKey).to.have.property('kid'); + expect(privateKey).to.have.property('kty', 'oct'); + }); + }); + + describe('privateKeyToBytes()', () => { + it('returns a private key as a byte array', async () => { + const privateKey = await XChaCha20.generateKey(); + const privateKeyBytes = await XChaCha20.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + }); + + it('returns the expected byte array for JWK input', async () => { + const privateKey: PrivateKeyJwk = { + k : 'L71Sr1mAvThwzcPzY0mArp0VszRA9j95eZ64yiMpEX8', + kty : 'oct', + kid : '6oEQ2tFk2QI4_Lz8uxQpT4_Qce6f9ceS3ZD76nqd_qg' + }; + const privateKeyBytes = await XChaCha20.privateKeyToBytes({ privateKey }); + + expect(privateKeyBytes).to.be.an.instanceOf(Uint8Array); + const expectedOutput = Convert.hex('2fbd52af5980bd3870cdc3f3634980ae9d15b33440f63f79799eb8ca2329117f').toUint8Array(); + expect(privateKeyBytes).to.deep.equal(expectedOutput); + }); + + it('throws an error when provided an asymmetric public key', async () => { + const publicKey: PublicKeyJwk = { + crv : 'Ed25519', + kty : 'OKP', + x : 'PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw', + }; + + await expect( + // @ts-expect-error because a public key is being passed to a method that expects a private key. + XChaCha20.privateKeyToBytes({ privateKey: publicKey }) + ).to.eventually.be.rejectedWith(Error, 'provided key is not a valid oct private key'); + }); + }); +}); \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519.ts b/packages/crypto/tests/fixtures/test-vectors/ed25519.ts deleted file mode 100644 index 261a00e56..000000000 --- a/packages/crypto/tests/fixtures/test-vectors/ed25519.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const ed25519TestVectors = [ - { - id : '1', - privateKey : { - encoded: '4f2db3cf791aaa1a6445490117b1a110435394f4bef8e384c64dce9536053c5b' - }, - publicKey: { - encoded: 'b3cf2b4a6852f156ab1536c204ca6f2eed787bd44f4295104dcb9b6df8329386' - } - }, - { - id : '2', - privateKey : { - encoded: '9b65f2e65734d4f8b338e4a4c81457289564056890d3c539143ab292ad31ee87' - }, - publicKey: { - encoded: '20e95ce23a1b76d8538e8404f9ce22f2d5a0daaa327b07741a07f116e0e87e6e' - } - }, -]; \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-private-key.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-private-key.json new file mode 100644 index 000000000..d02b92e56 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-private-key.json @@ -0,0 +1,44 @@ +{ + "description" : "Ed25519 bytesToPrivateKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected private key", + "input" : { + "privateKeyBytes": "add4bb8103785baf9ac534258e8aaf65f5f1adb5ef5f3df19bb80ab989c4d64b" + }, + "output": { + "crv" : "Ed25519", + "d" : "rdS7gQN4W6-axTQljoqvZfXxrbXvXz3xm7gKuYnE1ks", + "kid" : "whzXN9WPd2qZyKXCZmDMlU5TGQjzRHZa496Dj1K0ZAs", + "kty" : "OKP", + "x" : "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo" + } + }, + { + "description" : "converts wycheproof vector 2 to the expected private key", + "input" : { + "privateKeyBytes": "0a23a20072891237aa0864b5765139514908787878cd77135a0059881d313f00" + }, + "output": { + "crv" : "Ed25519", + "d" : "CiOiAHKJEjeqCGS1dlE5UUkIeHh4zXcTWgBZiB0xPwA", + "kid" : "e1A_70UPZow2psBhMg1fT7K5FfRtkrK-PJKgGbWB6Uk", + "kty" : "OKP", + "x" : "oSwr63cmXyqslTtQCTSdlBVaA62kFqrUUTGUgOmDykw" + } + }, + { + "description" : "converts wycheproof vector 3 to the expected private key", + "input" : { + "privateKeyBytes": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + }, + "output": { + "crv" : "Ed25519", + "d" : "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "kid" : "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty" : "OKP", + "x" : "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-public-key.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-public-key.json new file mode 100644 index 000000000..758aa76f6 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-public-key.json @@ -0,0 +1,41 @@ +{ + "description" : "Ed25519 bytesToPublicKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected public key", + "input" : { + "publicKeyBytes": "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" + }, + "output": { + "crv" : "Ed25519", + "kid" : "whzXN9WPd2qZyKXCZmDMlU5TGQjzRHZa496Dj1K0ZAs", + "kty" : "OKP", + "x" : "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo" + } + }, + { + "description" : "converts wycheproof vector 2 to the expected public key", + "input" : { + "publicKeyBytes": "a12c2beb77265f2aac953b5009349d94155a03ada416aad451319480e983ca4c" + }, + "output": { + "crv" : "Ed25519", + "kid" : "e1A_70UPZow2psBhMg1fT7K5FfRtkrK-PJKgGbWB6Uk", + "kty" : "OKP", + "x" : "oSwr63cmXyqslTtQCTSdlBVaA62kFqrUUTGUgOmDykw" + } + }, + { + "description" : "converts wycheproof vector 3 to the expected public key", + "input" : { + "publicKeyBytes": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + }, + "output": { + "crv" : "Ed25519", + "kid" : "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty" : "OKP", + "x" : "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/compute-public-key.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/compute-public-key.json new file mode 100644 index 000000000..81be96591 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/compute-public-key.json @@ -0,0 +1,73 @@ +{ + "description" : "Ed25519 computePublicKey test vectors", + "vectors" : [ + { + "description" : "computes the expected public key from the RFC8037, Appendex A.1 vector", + "input" : { + "privateKey": { + "crv": "Ed25519", + "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "kty": "OKP", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "output": { + "crv": "Ed25519", + "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty": "OKP", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + { + "description" : "computes the expected public key from wycheproof vector 1", + "input" : { + "privateKey": { + "crv": "Ed25519", + "d": "rdS7gQN4W6-axTQljoqvZfXxrbXvXz3xm7gKuYnE1ks", + "kty": "OKP", + "x": "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo" + } + }, + "output": { + "crv": "Ed25519", + "kid": "whzXN9WPd2qZyKXCZmDMlU5TGQjzRHZa496Dj1K0ZAs", + "kty": "OKP", + "x": "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo" + } + }, + { + "description" : "computes the expected public key from wycheproof vector 2", + "input" : { + "privateKey": { + "crv": "Ed25519", + "d": "CiOiAHKJEjeqCGS1dlE5UUkIeHh4zXcTWgBZiB0xPwA", + "kty": "OKP", + "x": "oSwr63cmXyqslTtQCTSdlBVaA62kFqrUUTGUgOmDykw" + } + }, + "output": { + "crv": "Ed25519", + "kid": "e1A_70UPZow2psBhMg1fT7K5FfRtkrK-PJKgGbWB6Uk", + "kty": "OKP", + "x": "oSwr63cmXyqslTtQCTSdlBVaA62kFqrUUTGUgOmDykw" + } + }, + { + "description" : "computes the expected public key from wycheproof vector 3", + "input" : { + "privateKey": { + "crv": "Ed25519", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "kty": "OKP", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "output": { + "crv": "Ed25519", + "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty": "OKP", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/convert-private-key-to-x25519.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/convert-private-key-to-x25519.json new file mode 100644 index 000000000..80f96df57 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/convert-private-key-to-x25519.json @@ -0,0 +1,41 @@ +{ + "description" : "Ed25519 convertPrivateKeyToX25519 test vectors", + "vectors" : [ + { + "description" : "converts Ed25519 private key to expected X25519 private key 1", + "input" : { + "privateKey": { + "kty": "OKP", + "crv": "Ed25519", + "d": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo", + "x": "0KTOwPi1C6HpNuxWFUVKqX37J4ZPXxdgivLLsQVI8bM" + } + }, + "output": { + "crv": "X25519", + "d": "qM1E646TMZwFcLwRAFwOAYnTT_AvbBd3NBGtGRKTyU8", + "kid": "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I", + "kty": "OKP", + "x": "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY" + } + }, + { + "description" : "converts Ed25519 private key to expected X25519 private key 2", + "input" : { + "privateKey": { + "kty": "OKP", + "crv": "Ed25519", + "d": "aeJsLIvfKVFAqRpCzldslD-NXtKItqK0jAs2vOxbGcA", + "x": "0nQq-UcELyC07P95hpWOOChIzrWhLC5P8I-EtoyBvwI" + } + }, + "output": { + "crv": "X25519", + "d": "oJofguv460IW5L3-7FmN3MttwqHTN3vbn5kJbbqxmGw", + "kid": "2o1RtaNsesiPxOTA4nqjKi8tuFPK4goGW5geg8R8YYA", + "kty": "OKP", + "x": "2gWwpR90WzM8pF6c2u4hVkisC9Z_vibouROB-bRpVhc" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/convert-public-key-to-x25519.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/convert-public-key-to-x25519.json new file mode 100644 index 000000000..ac57a8243 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/convert-public-key-to-x25519.json @@ -0,0 +1,37 @@ +{ + "description" : "Ed25519 convertPublicKeyToX25519 test vectors", + "vectors" : [ + { + "description" : "converts Ed25519 public key to expected X25519 public key 1", + "input" : { + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "0KTOwPi1C6HpNuxWFUVKqX37J4ZPXxdgivLLsQVI8bM" + } + }, + "output": { + "crv": "X25519", + "kid": "xtsuKULPh6VN9fuJMRwj66cDfQyLaxuXHkMlmAe_v6I", + "kty": "OKP", + "x": "7XdJtNmJ9pV_O_3mxWdn6YjiHJ-HhNkdYQARzVU_mwY" + } + }, + { + "description" : "converts Ed25519 public key to expected X25519 public key 2", + "input" : { + "publicKey": { + "kty": "OKP", + "crv": "Ed25519", + "x": "kZoyeYylO0RwCTgPuX-MG0V2YcqXOBVWL5TwCsWhkzU" + } + }, + "output": { + "crv": "X25519", + "kid": "O7xmoK9ptzlG6ewnl1cQ-6DbVgzIeMhP4nHUM8oD_M8", + "kty": "OKP", + "x": "cz3yKnz6Wfc1GchUbCDR-Pp2jZeXLTehnzPTsBJ_J14" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/private-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/private-key-to-bytes.json new file mode 100644 index 000000000..d30d2c21b --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/private-key-to-bytes.json @@ -0,0 +1,44 @@ +{ + "description" : "Ed25519 privateKeyToBytes test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "Ed25519", + "d" : "rdS7gQN4W6-axTQljoqvZfXxrbXvXz3xm7gKuYnE1ks", + "kid" : "whzXN9WPd2qZyKXCZmDMlU5TGQjzRHZa496Dj1K0ZAs", + "kty" : "OKP", + "x" : "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo" + } + }, + "output": "add4bb8103785baf9ac534258e8aaf65f5f1adb5ef5f3df19bb80ab989c4d64b" + }, + { + "description" : "converts wycheproof vector 2 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "Ed25519", + "d" : "CiOiAHKJEjeqCGS1dlE5UUkIeHh4zXcTWgBZiB0xPwA", + "kid" : "e1A_70UPZow2psBhMg1fT7K5FfRtkrK-PJKgGbWB6Uk", + "kty" : "OKP", + "x" : "oSwr63cmXyqslTtQCTSdlBVaA62kFqrUUTGUgOmDykw" + } + }, + "output": "0a23a20072891237aa0864b5765139514908787878cd77135a0059881d313f00" + }, + { + "description" : "converts wycheproof vector 3 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "Ed25519", + "d" : "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "kid" : "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty" : "OKP", + "x" : "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "output": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/public-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/public-key-to-bytes.json new file mode 100644 index 000000000..6ddfb0fd4 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/public-key-to-bytes.json @@ -0,0 +1,41 @@ +{ + "description" : "Ed25519 publicKeyToBytes test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "Ed25519", + "kid" : "whzXN9WPd2qZyKXCZmDMlU5TGQjzRHZa496Dj1K0ZAs", + "kty" : "OKP", + "x" : "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo" + } + }, + "output": "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" + }, + { + "description" : "converts wycheproof vector 2 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "Ed25519", + "kid" : "e1A_70UPZow2psBhMg1fT7K5FfRtkrK-PJKgGbWB6Uk", + "kty" : "OKP", + "x" : "oSwr63cmXyqslTtQCTSdlBVaA62kFqrUUTGUgOmDykw" + } + }, + "output": "a12c2beb77265f2aac953b5009349d94155a03ada416aad451319480e983ca4c" + }, + { + "description" : "converts wycheproof vector 3 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "Ed25519", + "kid" : "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty" : "OKP", + "x" : "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "output": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/sign.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/sign.json new file mode 100644 index 000000000..89af1f38f --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/sign.json @@ -0,0 +1,61 @@ +{ + "description": "Ed25519 sign test vectors", + "vectors": [ + { + "description": "generates the expected signature given the RFC8032 0x9d... key and empty message", + "input": { + "data": "", + "key": { + "crv": "Ed25519", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty": "OKP", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "output": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + { + "description": "generates the expected signature given the RFC8032 0x4c... key and 72 message", + "input": { + "data": "72", + "key": { + "crv": "Ed25519", + "d": "TM0Imyj_ltqdtsNG7BFOD1uKMZ81q6Yk2oz27U-4pvs", + "kid": "FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk", + "kty": "OKP", + "x": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw" + } + }, + "output": "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00" + }, + { + "description": "generates the expected signature given the RFC8032 0x00... key and 5a... message", + "input": { + "data": "5ac1dfc324f43e6cb79a87ab0470fa857b51fb944982e19074ca44b1e40082c1d07b92efa7ea55ad42b7c027e0b9e33756d95a2c1796a7c2066811dc41858377d4b835c1688d638884cd2ad8970b74c1a54aadd27064163928a77988b24403aa85af82ceab6b728e554761af7175aeb99215b7421e4474c04d213e01ff03e3529b11077cdf28964b8c49c5649e3a46fa0a09dcd59dcad58b9b922a83210acd5e65065531400234f5e40cddcf9804968e3e9ac6f5c44af65001e158067fc3a660502d13fa8874fa93332138d9606bc41b4cee7edc39d753dae12a873941bb357f7e92a4498847d6605456cb8c0b425a47d7d3ca37e54e903a41e6450a35ebe5237c6f0c1bbbc1fd71fb7cd893d189850295c199b7d88af26bc8548975fda1099ffefee42a52f3428ddff35e0173d3339562507ac5d2c45bbd2c19cfe89b", + "key": { + "crv": "Ed25519", + "d": "AC_dH3ZBeTqwZLt6qEj3YufsbjMv_CburNoUGuM7F4M", + "kid": "M7TyrCUM12xZUUArpFOvdxvSN0CKasiRsxOIlVcyEaA", + "kty": "OKP", + "x": "d9HY66zRP04vikDijEpjvJzjv7aXFjNLyyijPrE0CGw" + } + }, + "output": "0df3aa0d0999ad3dc580378f52d152700d5b3b057f56a66f92112e441e1cb9123c66f18712c87efe22d2573777296241216904d7cdd7d5ea433928bd2872fa0c" + }, + { + "description": "generates the expected signature given the RFC8032 0xf5... key and long message", + "input": { + "data": "08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0", + "key": { + "crv": "Ed25519", + "d": "9eV2fPFTMZUXYw8iaHa4bIFgzFg7wBN0TGvyVfXMDuU", + "kty": "OKP", + "x": "J4EX_BRMcjQPZ9DyMW6Dhs7_vyskKMnFH-98WX8dQm4", + "kid": "lZI1vM7tnlYapaF5-cy86ptx0tT_8Av721hhiNB5ti4" + } + }, + "output": "0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/ed25519/verify.json b/packages/crypto/tests/fixtures/test-vectors/ed25519/verify.json new file mode 100644 index 000000000..cb23c7532 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/ed25519/verify.json @@ -0,0 +1,61 @@ +{ + "description" : "Ed25519 verify test vectors", + "vectors" : [ + { + "description" : "verifies the signature for the RFC8032 0x9d... key and empty message", + "input" : { + "data": "", + "key": { + "crv": "Ed25519", + "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "kty": "OKP", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }, + "signature": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + "output": true + }, + { + "description" : "verifies the signature for the RFC8032 0x4c... key and 72 message", + "input" : { + "data": "72", + "key": { + "crv": "Ed25519", + "kid": "FtIu-VbGrfe_KB6CH7GNwODB72MNxj_ml11dEvO-7kk", + "kty": "OKP", + "x": "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw" + }, + "signature": "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00" + }, + "output": true + }, + { + "description" : "verifies the signature for the RFC8032 0x00... key and 5a... message", + "input" : { + "data": "5ac1dfc324f43e6cb79a87ab0470fa857b51fb944982e19074ca44b1e40082c1d07b92efa7ea55ad42b7c027e0b9e33756d95a2c1796a7c2066811dc41858377d4b835c1688d638884cd2ad8970b74c1a54aadd27064163928a77988b24403aa85af82ceab6b728e554761af7175aeb99215b7421e4474c04d213e01ff03e3529b11077cdf28964b8c49c5649e3a46fa0a09dcd59dcad58b9b922a83210acd5e65065531400234f5e40cddcf9804968e3e9ac6f5c44af65001e158067fc3a660502d13fa8874fa93332138d9606bc41b4cee7edc39d753dae12a873941bb357f7e92a4498847d6605456cb8c0b425a47d7d3ca37e54e903a41e6450a35ebe5237c6f0c1bbbc1fd71fb7cd893d189850295c199b7d88af26bc8548975fda1099ffefee42a52f3428ddff35e0173d3339562507ac5d2c45bbd2c19cfe89b", + "key": { + "crv": "Ed25519", + "kid": "M7TyrCUM12xZUUArpFOvdxvSN0CKasiRsxOIlVcyEaA", + "kty": "OKP", + "x": "d9HY66zRP04vikDijEpjvJzjv7aXFjNLyyijPrE0CGw" + }, + "signature": "0df3aa0d0999ad3dc580378f52d152700d5b3b057f56a66f92112e441e1cb9123c66f18712c87efe22d2573777296241216904d7cdd7d5ea433928bd2872fa0c" + }, + "output": true + }, + { + "description" : "verifies the signature for the RFC8032 0xf5... key and long message", + "input" : { + "data": "08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0", + "key": { + "crv": "Ed25519", + "kty": "OKP", + "x": "J4EX_BRMcjQPZ9DyMW6Dhs7_vyskKMnFH-98WX8dQm4", + "kid": "lZI1vM7tnlYapaF5-cy86ptx0tT_8Av721hhiNB5ti4" + }, + "signature": "0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03" + }, + "output": true + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/jose.ts b/packages/crypto/tests/fixtures/test-vectors/jose.ts index 7aeeca461..c5b0de666 100644 --- a/packages/crypto/tests/fixtures/test-vectors/jose.ts +++ b/packages/crypto/tests/fixtures/test-vectors/jose.ts @@ -1,317 +1,3 @@ -export const cryptoKeyPairToJsonWebKeyTestVectors = [ - { - id : 'ckp.jwk.1', - cryptoKey : { - publicKey: { - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - material : '02c6cf53ccfc13fbdfb25d827636839d9874df3148eba88c07f07601645ca5a006', // Hex, compressed - type : 'public', - usages : ['verify'], - }, - privateKey: { - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - material : '1d70915381c9bcb940752c3892b6c3b4476a6906b6aee839227f3f38eaf91190', // Hex - type : 'private', - usages : ['sign'], - } - }, - jsonWebKey: { - publicKeyJwk: { - 'alg' : 'ES256K', - 'crv' : 'secp256k1', - 'ext' : 'true', - 'key_ops' : ['verify'], - 'kty' : 'EC', - 'x' : 'xs9TzPwT-9-yXYJ2NoOdmHTfMUjrqIwH8HYBZFyloAY', // Base64url - 'y' : 'tMa4vfJC9rR8S87Sx9yEHACYOWOh7_UWLiFal56lObY', // Base64url - }, - privateKeyJwk: { - 'alg' : 'ES256K', - 'crv' : 'secp256k1', - 'd' : 'HXCRU4HJvLlAdSw4krbDtEdqaQa2rug5In8_OOr5EZA', // Base64url - 'ext' : 'true', - 'key_ops' : ['sign'], - 'kty' : 'EC', - 'x' : 'xs9TzPwT-9-yXYJ2NoOdmHTfMUjrqIwH8HYBZFyloAY', // Base64url - 'y' : 'tMa4vfJC9rR8S87Sx9yEHACYOWOh7_UWLiFal56lObY', // Base64url - }, - } - }, - { - id : 'ckp.jwk.2', - cryptoKey : { - publicKey: { - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - material : '045d67b538b1f3dc38326a975b17c4312b7620c39b656b3012dc9205c5804870c7ab53846c0b4c6f6c0267f08b9ac7075fe1f0b617d013630d92a3c760908b71e3', // Hex, uncompressed - type : 'public', - usages : ['verify'], - }, - privateKey: { - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : true, - material : 'c1f488e4919027f1da827a3f25c8121f9092f5d940c0da9a52cb36e192fa1610', // Hex - type : 'private', - usages : ['sign'], - } - }, - jsonWebKey: { - publicKeyJwk: { - 'alg' : 'ES256K', - 'crv' : 'secp256k1', - 'ext' : 'true', - 'key_ops' : ['verify'], - 'kty' : 'EC', - 'x' : 'XWe1OLHz3DgyapdbF8QxK3Ygw5tlazAS3JIFxYBIcMc', // Base64url - 'y' : 'q1OEbAtMb2wCZ_CLmscHX-HwthfQE2MNkqPHYJCLceM', // Base64url - }, - privateKeyJwk: { - 'alg' : 'ES256K', - 'crv' : 'secp256k1', - 'd' : 'wfSI5JGQJ_Hagno_JcgSH5CS9dlAwNqaUss24ZL6FhA', // Base64url - 'ext' : 'true', - 'key_ops' : ['sign'], - 'kty' : 'EC', - 'x' : 'XWe1OLHz3DgyapdbF8QxK3Ygw5tlazAS3JIFxYBIcMc', // Base64url - 'y' : 'q1OEbAtMb2wCZ_CLmscHX-HwthfQE2MNkqPHYJCLceM', // Base64url - }, - } - }, - { - id : 'ckp.jwk.3', - cryptoKey : { - publicKey: { - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : true, - material : 'ae92a70cff05e3f8f0bd0ef10e492e2b1d7ae4e4b0732ad0be61169767a28085', // Hex - type : 'public', - usages : ['verify'], - }, - privateKey: { - algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, - extractable : true, - material : 'f69e3da1db3fc8b7474224e3271099dab537807212477ad034ae52f3e39d8782', // Hex - type : 'private', - usages : ['sign'], - } - }, - jsonWebKey: { - publicKeyJwk: { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'ext' : 'true', - 'key_ops' : ['verify'], - 'kty' : 'OKP', - 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU', // Base64url - }, - privateKeyJwk: { - 'alg' : 'EdDSA', - 'crv' : 'Ed25519', - 'd' : '9p49ods_yLdHQiTjJxCZ2rU3gHISR3rQNK5S8-Odh4I', // Base64url - 'ext' : 'true', - 'key_ops' : ['sign'], - 'kty' : 'OKP', - 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU', // Base64url - }, - } - }, - { - id : 'ckp.jwk.4', - cryptoKey : { - publicKey: { - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : true, - material : '796037a1434a9b79d9374bea882fed0a53c2901ce737947463d3687c99286973', // Hex - type : 'public', - usages : ['deriveBits', 'deriveKey'], - }, - privateKey: { - algorithm : { name: 'ECDH', namedCurve: 'X25519' }, - extractable : true, - material : '20a6d2ab343efc5d8718af1afb3157984b63712edc5f5c1c77bcf8f732f8b545', // Hex - type : 'private', - usages : ['deriveBits', 'deriveKey'], - } - }, - jsonWebKey: { - publicKeyJwk: { - 'crv' : 'X25519', - 'ext' : 'true', - 'key_ops' : ['deriveBits', 'deriveKey'], - 'kty' : 'OKP', - 'x' : 'eWA3oUNKm3nZN0vqiC_tClPCkBznN5R0Y9NofJkoaXM', // Base64url - }, - privateKeyJwk: { - 'crv' : 'X25519', - 'd' : 'IKbSqzQ-_F2HGK8a-zFXmEtjcS7cX1wcd7z49zL4tUU', // Base64url - 'ext' : 'true', - 'key_ops' : ['deriveBits', 'deriveKey'], - 'kty' : 'OKP', - 'x' : 'eWA3oUNKm3nZN0vqiC_tClPCkBznN5R0Y9NofJkoaXM', // Base64url - }, - } - }, -]; - -export const cryptoKeyToJwkTestVectors = [ - { - id : 'csk.jwk.1', - cryptoKey : { - algorithm : { name: 'AES-CTR', length: 256 }, - extractable : true, - material : '510b48012fab99607ebe03601b894fae74d2dad36fc033ca97daecd0bf480a75', // Hex - type : 'secret', - usages : ['encrypt', 'decrypt'], - }, - jsonWebKey: { - 'alg' : 'A256CTR', - 'ext' : 'true', - 'key_ops' : ['encrypt', 'decrypt'], - 'k' : 'UQtIAS-rmWB-vgNgG4lPrnTS2tNvwDPKl9rs0L9ICnU', // Base64url - 'kty' : 'oct', - } - }, - { - id : 'csk.jwk.2', - cryptoKey : { - algorithm : { name: 'AES-GCM', length: 256 }, - extractable : true, - material : 'fa919d00b0edc66c73efcc2325073fff8173bd30956174cd50b3381f438a56ac', // Hex - type : 'secret', - usages : ['encrypt', 'decrypt'], - }, - jsonWebKey: { - 'alg' : 'A256GCM', - 'ext' : 'true', - 'key_ops' : ['encrypt', 'decrypt'], - 'k' : '-pGdALDtxmxz78wjJQc__4FzvTCVYXTNULM4H0OKVqw', // Base64url - 'kty' : 'oct', - } - }, - { - id : 'csk.jwk.3', - cryptoKey : { - algorithm : { name: 'HMAC', hash: { name: 'SHA-256' } }, - extractable : true, - material : 'dc739a7be3ffc152af69bc45dfb02d81cfe313c7cb074c643144a9c15588d87468bafa02da20ab7fc8f7498916b184459b84aff27736be9cc8f60e49ca0d01c7', // Hex - type : 'secret', - usages : ['sign', 'verify'], - }, - jsonWebKey: { - 'alg' : 'HS256', - 'ext' : 'true', - 'key_ops' : ['sign', 'verify'], - 'k' : '3HOae-P_wVKvabxF37Atgc_jE8fLB0xkMUSpwVWI2HRouvoC2iCrf8j3SYkWsYRFm4Sv8nc2vpzI9g5Jyg0Bxw', // Base64url - 'kty' : 'oct', - } - }, -]; - -export const joseToWebCryptoTestVectors = [ - { - id : 'jose.wc.1', - jose : { crv: 'Ed25519', alg: 'EdDSA', kty: 'OKP' }, - webCrypto : { namedCurve: 'Ed25519', name: 'EdDSA' } - }, - { - id : 'jose.wc.2', - jose : { crv: 'Ed448', alg: 'EdDSA', kty: 'OKP' }, - webCrypto : { namedCurve: 'Ed448', name: 'EdDSA' } - }, - { - id : 'jose.wc.3', - jose : { crv: 'X25519', kty: 'OKP' }, - webCrypto : { namedCurve: 'X25519', name: 'ECDH' } - }, - { - id : 'jose.wc.4', - jose : { crv: 'secp256k1', alg: 'ES256K', kty: 'EC' }, - webCrypto : { namedCurve: 'secp256k1', name: 'ECDSA' } - }, - { - id : 'jose.wc.5', - jose : { crv: 'secp256k1', kty: 'EC' }, - webCrypto : { namedCurve: 'secp256k1', name: 'ECDH' } - }, - { - id : 'jose.wc.6', - jose : { crv: 'P-256', alg: 'ES256', kty: 'EC' }, - webCrypto : { namedCurve: 'P-256', name: 'ECDSA' } - }, - { - id : 'jose.wc.7', - jose : { crv: 'P-384', alg: 'ES384', kty: 'EC' }, - webCrypto : { namedCurve: 'P-384', name: 'ECDSA' } - }, - { - id : 'jose.wc.8', - jose : { crv: 'P-521', alg: 'ES512', kty: 'EC' }, - webCrypto : { namedCurve: 'P-521', name: 'ECDSA' } - }, - { - id : 'jose.wc.9', - jose : { alg: 'A128CBC', kty: 'oct' }, - webCrypto : { name: 'AES-CBC', length: 128 } - }, - { - id : 'jose.wc.10', - jose : { alg: 'A192CBC', kty: 'oct' }, - webCrypto : { name: 'AES-CBC', length: 192 } - }, - { - id : 'jose.wc.11', - jose : { alg: 'A256CBC', kty: 'oct' }, - webCrypto : { name: 'AES-CBC', length: 256 } - }, - { - id : 'jose.wc.12', - jose : { alg: 'A128CTR', kty: 'oct' }, - webCrypto : { name: 'AES-CTR', length: 128 } - }, - { - id : 'jose.wc.13', - jose : { alg: 'A192CTR', kty: 'oct' }, - webCrypto : { name: 'AES-CTR', length: 192 } - }, - { - id : 'jose.wc.14', - jose : { alg: 'A256CTR', kty: 'oct' }, - webCrypto : { name: 'AES-CTR', length: 256 } - }, - { - id : 'jose.wc.15', - jose : { alg: 'A128GCM', kty: 'oct' }, - webCrypto : { name: 'AES-GCM', length: 128 } - }, - { - id : 'jose.wc.16', - jose : { alg: 'A192GCM', kty: 'oct' }, - webCrypto : { name: 'AES-GCM', length: 192 } - }, - { - id : 'jose.wc.17', - jose : { alg: 'A256GCM', kty: 'oct' }, - webCrypto : { name: 'AES-GCM', length: 256 } - }, - { - id : 'jose.wc.18', - jose : { alg: 'HS256', kty: 'oct' }, - webCrypto : { name: 'HMAC', hash: { name: 'SHA-256' } } - }, - { - id : 'jose.wc.19', - jose : { alg: 'HS384', kty: 'oct' }, - webCrypto : { name: 'HMAC', hash: { name: 'SHA-384' } } - }, - { - id : 'jose.wc.20', - jose : { alg: 'HS512', kty: 'oct' }, - webCrypto : { name: 'HMAC', hash: { name: 'SHA-512' } } - }, -]; - export const joseToMulticodecTestVectors = [ { output : { code: 237, name: 'ed25519-pub' }, @@ -372,258 +58,6 @@ export const joseToMulticodecTestVectors = [ }, ]; -export const keyToJwkTestVectorsKeyMaterial = '72e63e7c4bbf575b386fc1db1b3cbff5539a36dc6250fccb9fa28e013773d24b'; -export const keyToJwkMulticodecTestVectors = [ - { - input : 'ed25519-pub', - output : { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : 'ed25519-priv', - output : { - d : '', - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'c5UR1q2r1lOT_ygDhSkU3paf5Bmukg-jX-1t4kIKJvA' - } - }, - { - input : 'secp256k1-pub', - output : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - input : 'secp256k1-priv', - output : { - d : '', - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - input : 'x25519-pub', - output : { - crv : 'X25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : 'x25519-priv', - output : { - d : '', - crv : 'X25519', - kty : 'OKP', - x : 'MBZd77wAy5932AEP7MHXOevv_MLzzD9OP_fZAOlnIWM' - } - } -]; - -export const keyToJwkWebCryptoTestVectors = [ - { - input : { namedCurve: 'Ed25519', name: 'EdDSA' }, - output : { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : { namedCurve: 'secp256k1', name: 'ECDSA' }, - output : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', - } - }, - { - input : { namedCurve: 'X25519', name: 'ECDH' }, - output : { - crv : 'X25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : { namedCurve: 'secp256k1', name: 'ECDSA' }, - output : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - input : { namedCurve: 'secp256k1', name: 'ECDH' }, - output : { - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - input : { name: 'AES-CBC', length: 128 }, - output : { - alg : 'A128CBC', - kty : 'oct', - k : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : { name: 'HMAC', hash: { name: 'SHA-256' } }, - output : { - alg : 'HS256', - kty : 'oct', - k : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - } -]; - -export const keyToJwkWebCryptoWithNullKTYTestVectors = [ - { - input : { namedCurve: 'Ed25519', name: 'EdDSA' }, - output : { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : { namedCurve: 'secp256k1', name: 'ECDSA' }, - output : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', - } - }, - { - input : { namedCurve: 'X25519', name: 'ECDH' }, - output : { - crv : 'X25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - input : { namedCurve: 'secp256k1', name: 'ECDSA' }, - output : { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - input : { namedCurve: 'secp256k1', name: 'ECDH' }, - output : { - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - input : { name: 'AES-CBC', length: 128 }, - output : { - alg : 'A128CBC', - kty : null, - } - }, - { - input : { name: 'HMAC', hash: { name: 'SHA-256' } }, - output : { - alg : 'HS256', - kty : null, - } - } -]; - -export const jwkToKeyTestVectors = [ - { - output: { - keyMaterial : keyToJwkTestVectorsKeyMaterial, - keyType : 'public', - }, - input: { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - output: { - keyMaterial : '04fd38a116fe6ddb88635ac45c75905e1096bae61401e5a88e6261ba98cbb5459051f88e19c92126e87d7f7f988bb83e8d320b60feaf11639217576bc2304779b0', - keyType : 'public', - }, - input: { - alg : 'ES256K', - crv : 'secp256k1', - kty : 'EC', - x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', - y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA' - } - }, - { - output: { - keyMaterial : keyToJwkTestVectorsKeyMaterial, - keyType : 'private', - }, - input: { - alg : 'A128CBC', - kty : 'oct', - k : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - output: { - keyMaterial : keyToJwkTestVectorsKeyMaterial, - keyType : 'private', - }, - input: { - alg : 'HS256', - kty : 'oct', - k : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks' - } - }, - { - output: { - keyMaterial : '', - keyType : 'private', - }, - input: { - d : '', - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - x : 'c5UR1q2r1lOT_ygDhSkU3paf5Bmukg-jX-1t4kIKJvA', - }, - } -]; - export const jwkToThumbprintTestVectors = [ { output : 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs', @@ -672,80 +106,30 @@ export const jwkToThumbprintTestVectors = [ }, ]; -export const jwkToCryptoKeyTestVectors = [ - { - cryptoKey: { - algorithm : { name: 'AES-CTR', length: 256 }, - extractable : true, - type : 'private', - usages : ['encrypt', 'decrypt'], - }, - jsonWebKey: { - 'alg' : 'A256CTR', - 'ext' : 'true', - 'key_ops' : ['encrypt', 'decrypt'], - 'k' : 'UQtIAS-rmWB-vgNgG4lPrnTS2tNvwDPKl9rs0L9ICnU', // Base64url - 'kty' : 'oct', - } - }, - { - cryptoKey: { - algorithm : { name: 'AES-GCM', length: 256 }, - extractable : false, - type : 'private', - usages : ['encrypt', 'decrypt'], - }, - jsonWebKey: { - 'alg' : 'A256GCM', - 'ext' : 'false', - 'key_ops' : ['encrypt', 'decrypt'], - 'k' : '-pGdALDtxmxz78wjJQc__4FzvTCVYXTNULM4H0OKVqw', // Base64url - 'kty' : 'oct', - } - }, - { - cryptoKey: { - algorithm : { name: 'HMAC', hash: { name: 'SHA-256' } }, - extractable : true, - type : 'private', - usages : ['sign', 'verify'], - }, - jsonWebKey: { - 'alg' : 'HS256', - 'ext' : 'true', - 'key_ops' : ['sign', 'verify'], - 'k' : '3HOae-P_wVKvabxF37Atgc_jE8fLB0xkMUSpwVWI2HRouvoC2iCrf8j3SYkWsYRFm4Sv8nc2vpzI9g5Jyg0Bxw', // Base64url - 'kty' : 'oct', - } - }, -]; - export const jwkToMultibaseIdTestVectors = [ { - output : 'zQ3sheTFzDvGpXAc9AXtwGF3MW1CusKovnwM4pSsUamqKCyLB', - input : { - alg : 'ES256K', + input: { crv : 'secp256k1', kty : 'EC', x : '_TihFv5t24hjWsRcdZBeEJa65hQB5aiOYmG6mMu1RZA', y : 'UfiOGckhJuh9f3-Yi7g-jTILYP6vEWOSF1drwjBHebA', }, + output: 'zQ3sheTFzDvGpXAc9AXtwGF3MW1CusKovnwM4pSsUamqKCyLB', }, { - output : 'z6LSjQhGhqqYgrFsNFoZL9wzuKpS1xQ7YNE6fnLgSyW2hUt2', - input : { + input: { crv : 'X25519', kty : 'OKP', x : 'cuY-fEu_V1s4b8HbGzy_9VOaNtxiUPzLn6KOATdz0ks', }, + output: 'z6LSjQhGhqqYgrFsNFoZL9wzuKpS1xQ7YNE6fnLgSyW2hUt2', }, { - output : 'zAuT', - input : { - d : '', - crv : 'X25519', + input: { + crv : 'Ed25519', kty : 'OKP', - x : 'MBZd77wAy5932AEP7MHXOevv_MLzzD9OP_fZAOlnIWM', + x : 'wwk7wOlocpOHDopgc0cZVCnl_7zFrp-JpvZe9vr5500' }, + output: 'z6MksabiHWJ5wQqJGDzxw1EiV5zi6BE6QRENTnHBcKHSqLaQ', }, ]; diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1.ts b/packages/crypto/tests/fixtures/test-vectors/secp256k1.ts deleted file mode 100644 index f6b63dfea..000000000 --- a/packages/crypto/tests/fixtures/test-vectors/secp256k1.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const secp256k1TestVectors = [ - { - id : '1', - privateKey : { - encoded: '740ec69810de9ad1b8f298f1d2c0e6a52dd1e958dc2afc85764bec169c222e88' - }, - publicKey: { - encoded : '043752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553', - x : '3752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258', - y : 'c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553' - } - }, - { - id : '2', - privateKey : { - encoded: 'b4713736a3562ff9b7b9e56ad2a533f241610e88217536cf2e620967daf91fd4' - }, - publicKey: { - encoded : '042dc60f3eed21d861fe7ccd353fb87e4dba6d0f453a71d4f8a1d2a17fe5486fd72a1d3bb247c3ca44e2a4d94cc616d6ce991fca220262c51d4edfcd3a1f55c9b4', - x : '2dc60f3eed21d861fe7ccd353fb87e4dba6d0f453a71d4f8a1d2a17fe5486fd7', - y : '2a1d3bb247c3ca44e2a4d94cc616d6ce991fca220262c51d4edfcd3a1f55c9b4' - }, - }, - { - id : '3', - privateKey : { - encoded: 'ea3db478f42cdbca7dbfe521167b03f40a5245370cba07142868c21d0082b391' - }, - publicKey: { - encoded : '037a549a3bdc432592ed40f2549e8668172e8bf1c3985066199472477f767b08f3', - x : '7a549a3bdc432592ed40f2549e8668172e8bf1c3985066199472477f767b08f3', - y : 'a5d03261db10f90f42af658c88f56aaf96fb1561f9c70f61ebe2c5bd2870b571' - } - }, - { - id : '4', - privateKey : { - encoded: '1b23fe831540368c37ec150febdaecc3dc47168585f3171a705f919357f9fff7' - }, - publicKey: { - encoded : '02ea7dd6427cdc1bb1b79584cab8e8109bf98e1cfef6c8dc9d8005d8e49ef1c150', - x : 'ea7dd6427cdc1bb1b79584cab8e8109bf98e1cfef6c8dc9d8005d8e49ef1c150', - y : 'e02763fa1504fa357acbb00c6711b8733c0a5938ebdaf228abd6ccbe7dbc6f80' - } - } -]; \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-private-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-private-key.json new file mode 100644 index 000000000..1ae1a4d9a --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-private-key.json @@ -0,0 +1,61 @@ +{ + "description" : "Secp256k1 bytesToPrivateKey test vectors", + "vectors" : [ + { + "description" : "converts noble ecdsa vector 1 to the expected private key", + "input" : { + "privateKeyBytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + "output": { + "crv" : "secp256k1", + "d" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE", + "kid" : "2JF8vg9etJzjFwZwmkvhBLLZ0bfMVVOPivYR5lFtcec", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg" + } + }, + { + "description" : "converts noble ecdsa vector 2 to the expected private key", + "input" : { + "privateKeyBytes": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140" + }, + "output": { + "crv" : "secp256k1", + "d" : "_____________________rqu3OavSKA7v9JejNA2QUA", + "kid" : "A5kvmZN8g_rnvmmIfgTaV8S8KUnA6plB3cCmCMUZPyQ", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "t8UliNlcO5qiWwQD8e73VwLoS7dZeqvmY7gvbwTvJ3c" + } + }, + { + "description" : "converts noble ecdsa vector 3 to the expected private key", + "input" : { + "privateKeyBytes": "00000000000000000000000000007246174ab1e92e9149c6e446fe194d072637" + }, + "output": { + "crv" : "secp256k1", + "d" : "AAAAAAAAAAAAAAAAAAByRhdKsekukUnG5Eb-GU0HJjc", + "kid" : "IJxfsScP6p4G8L9Yf8mYI8WIS87ulINKi0oUbS670KQ", + "kty" : "EC", + "x" : "tILZZWpOmaFc_atei0h8sHIG3xy4OvsHbG97qJCkNoE", + "y": "g-vgQCnVwDMAxAHbZapqnb1Hm05gvdGQoZzltVMgE9I" + } + }, + { + "description" : "converts private key bytes to the expected private key JWK", + "input" : { + "privateKeyBytes": "bb42227e72b0f2607c7810a814b6796da369e9dc22b85b739e0eb924b770cd51" + }, + "output": { + "crv" : "secp256k1", + "d" : "u0IifnKw8mB8eBCoFLZ5baNp6dwiuFtzng65JLdwzVE", + "kid" : "ikH9Jgh0U90gMJQ1txhlaST6VOdP_ygPJNN0JPaAwTI", + "kty" : "EC", + "x" : "9zVCTdMxpNyy3W1l0VfLdkpQyFdkXvDA0Jpx3TTn1og", + "y": "lNsXBQzhcrrGrf9XZYri_LLN1Bye4PdfTaoHbC6PIGI" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json new file mode 100644 index 000000000..31aa4b4a3 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json @@ -0,0 +1,57 @@ +{ + "description" : "Secp256k1 bytesToPublicKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected public key", + "input" : { + "publicKeyBytes": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" + }, + "output": { + "crv" : "secp256k1", + "kid" : "2JF8vg9etJzjFwZwmkvhBLLZ0bfMVVOPivYR5lFtcec", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg" + } + }, + { + "description" : "converts wycheproof vector 2 to the expected public key", + "input" : { + "publicKeyBytes": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777" + }, + "output": { + "crv" : "secp256k1", + "kid" : "A5kvmZN8g_rnvmmIfgTaV8S8KUnA6plB3cCmCMUZPyQ", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "t8UliNlcO5qiWwQD8e73VwLoS7dZeqvmY7gvbwTvJ3c" + } + }, + { + "description" : "converts wycheproof vector 3 to the expected public key", + "input" : { + "publicKeyBytes": "04b482d9656a4e99a15cfdab5e8b487cb07206df1cb83afb076c6f7ba890a4368183ebe04029d5c03300c401db65aa6a9dbd479b4e60bdd190a19ce5b5532013d2" + }, + "output": { + "crv" : "secp256k1", + "kid" : "IJxfsScP6p4G8L9Yf8mYI8WIS87ulINKi0oUbS670KQ", + "kty" : "EC", + "x" : "tILZZWpOmaFc_atei0h8sHIG3xy4OvsHbG97qJCkNoE", + "y": "g-vgQCnVwDMAxAHbZapqnb1Hm05gvdGQoZzltVMgE9I" + } + }, + { + "description" : "converts public key bytes to the expected public key JWK", + "input" : { + "publicKeyBytes": "04f735424dd331a4dcb2dd6d65d157cb764a50c857645ef0c0d09a71dd34e7d68894db17050ce172bac6adff57658ae2fcb2cdd41c9ee0f75f4daa076c2e8f2062" + }, + "output": { + "crv" : "secp256k1", + "kid" : "ikH9Jgh0U90gMJQ1txhlaST6VOdP_ygPJNN0JPaAwTI", + "kty" : "EC", + "x" : "9zVCTdMxpNyy3W1l0VfLdkpQyFdkXvDA0Jpx3TTn1og", + "y": "lNsXBQzhcrrGrf9XZYri_LLN1Bye4PdfTaoHbC6PIGI" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/get-curve-points.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/get-curve-points.json new file mode 100644 index 000000000..4ff58c133 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/get-curve-points.json @@ -0,0 +1,45 @@ +{ + "description" : "Secp256k1 getCurvePoints test vectors", + "vectors" : [ + { + "description" : "returns public key x and y coordinates given a public key", + "input" : { + "key": "043752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553" + }, + "output": { + "x": "3752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258", + "y": "c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553" + } + }, + { + "description" : "returns public key x and y coordinates given a private key", + "input" : { + "key": "740ec69810de9ad1b8f298f1d2c0e6a52dd1e958dc2afc85764bec169c222e88" + }, + "output": { + "x": "3752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258", + "y": "c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553" + } + }, + { + "description" : "handles private keys that require padded x-coordinate when converting from BigInt to bytes", + "input" : { + "key": "0206a1f9628c5bcd31f3bbc2f160ec98f99960147e04ea192f56c53a0086c5432d" + }, + "output": { + "x": "06a1f9628c5bcd31f3bbc2f160ec98f99960147e04ea192f56c53a0086c5432d", + "y": "bf2efab7943be51219a283c0979ccba0fbe03f571e75b0eb338cc2ec01e70552" + } + }, + { + "description" : "handles private keys that require padded y-coordinate when converting from BigInt to bytes", + "input" : { + "key": "032ff752fb8fc6af85c8682b0ca9d48901b2b9ac130f558bd1a9092240d42c4682" + }, + "output": { + "x": "2ff752fb8fc6af85c8682b0ca9d48901b2b9ac130f558bd1a9092240d42c4682", + "y": "048c39d9ebdc1fd98bda38b7f00b93de1d2af5bb3ba8cb532ad47c1f36e19501" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/private-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/private-key-to-bytes.json new file mode 100644 index 000000000..746d764ee --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/private-key-to-bytes.json @@ -0,0 +1,61 @@ +{ + "description" : "Secp256k1 privateKeyToBytes test vectors", + "vectors" : [ + { + "description" : "converts noble ecdsa vector 1 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "secp256k1", + "d" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE", + "kid" : "2JF8vg9etJzjFwZwmkvhBLLZ0bfMVVOPivYR5lFtcec", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg" + } + }, + "output": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "description" : "converts noble ecdsa vector 2 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "secp256k1", + "d" : "_____________________rqu3OavSKA7v9JejNA2QUA", + "kid" : "A5kvmZN8g_rnvmmIfgTaV8S8KUnA6plB3cCmCMUZPyQ", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "t8UliNlcO5qiWwQD8e73VwLoS7dZeqvmY7gvbwTvJ3c" + } + }, + "output": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140" + }, + { + "description" : "converts noble ecdsa vector 3 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "secp256k1", + "d" : "AAAAAAAAAAAAAAAAAAByRhdKsekukUnG5Eb-GU0HJjc", + "kid" : "IJxfsScP6p4G8L9Yf8mYI8WIS87ulINKi0oUbS670KQ", + "kty" : "EC", + "x" : "tILZZWpOmaFc_atei0h8sHIG3xy4OvsHbG97qJCkNoE", + "y": "g-vgQCnVwDMAxAHbZapqnb1Hm05gvdGQoZzltVMgE9I" + } + }, + "output": "00000000000000000000000000007246174ab1e92e9149c6e446fe194d072637" + }, + { + "description" : "converts byte array bytes to the expected byte array JWK", + "input" : { + "privateKey": { + "crv" : "secp256k1", + "d" : "u0IifnKw8mB8eBCoFLZ5baNp6dwiuFtzng65JLdwzVE", + "kid" : "ikH9Jgh0U90gMJQ1txhlaST6VOdP_ygPJNN0JPaAwTI", + "kty" : "EC", + "x" : "9zVCTdMxpNyy3W1l0VfLdkpQyFdkXvDA0Jpx3TTn1og", + "y": "lNsXBQzhcrrGrf9XZYri_LLN1Bye4PdfTaoHbC6PIGI" + } + }, + "output": "bb42227e72b0f2607c7810a814b6796da369e9dc22b85b739e0eb924b770cd51" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/public-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/public-key-to-bytes.json new file mode 100644 index 000000000..ee3e0a5a5 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/public-key-to-bytes.json @@ -0,0 +1,57 @@ +{ + "description" : "Secp256k1 publicKeyToBytes test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "secp256k1", + "kid" : "2JF8vg9etJzjFwZwmkvhBLLZ0bfMVVOPivYR5lFtcec", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg" + } + }, + "output": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" + }, + { + "description" : "converts wycheproof vector 2 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "secp256k1", + "kid" : "A5kvmZN8g_rnvmmIfgTaV8S8KUnA6plB3cCmCMUZPyQ", + "kty" : "EC", + "x" : "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", + "y": "t8UliNlcO5qiWwQD8e73VwLoS7dZeqvmY7gvbwTvJ3c" + } + }, + "output": "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798b7c52588d95c3b9aa25b0403f1eef75702e84bb7597aabe663b82f6f04ef2777" + }, + { + "description" : "converts wycheproof vector 3 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "secp256k1", + "kid" : "IJxfsScP6p4G8L9Yf8mYI8WIS87ulINKi0oUbS670KQ", + "kty" : "EC", + "x" : "tILZZWpOmaFc_atei0h8sHIG3xy4OvsHbG97qJCkNoE", + "y": "g-vgQCnVwDMAxAHbZapqnb1Hm05gvdGQoZzltVMgE9I" + } + }, + "output": "04b482d9656a4e99a15cfdab5e8b487cb07206df1cb83afb076c6f7ba890a4368183ebe04029d5c03300c401db65aa6a9dbd479b4e60bdd190a19ce5b5532013d2" + }, + { + "description" : "converts byte array bytes to the expected byte array JWK", + "input" : { + "publicKey": { + "crv" : "secp256k1", + "kid" : "ikH9Jgh0U90gMJQ1txhlaST6VOdP_ygPJNN0JPaAwTI", + "kty" : "EC", + "x" : "9zVCTdMxpNyy3W1l0VfLdkpQyFdkXvDA0Jpx3TTn1og", + "y": "lNsXBQzhcrrGrf9XZYri_LLN1Bye4PdfTaoHbC6PIGI" + } + }, + "output": "04f735424dd331a4dcb2dd6d65d157cb764a50c857645ef0c0d09a71dd34e7d68894db17050ce172bac6adff57658ae2fcb2cdd41c9ee0f75f4daa076c2e8f2062" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json new file mode 100644 index 000000000..f704b2f0e --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-private-key.json @@ -0,0 +1,33 @@ +{ + "description" : "Secp256k1 validatePrivateKey test vectors", + "vectors" : [ + { + "description" : "returns true for valid private keys", + "input" : { + "key": "740ec69810de9ad1b8f298f1d2c0e6a52dd1e958dc2afc85764bec169c222e88" + }, + "output": true + }, + { + "description" : "returns false for invalid private keys", + "input" : { + "key": "02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" + }, + "output": false + }, + { + "description" : "returns false if an compressed public key is given", + "input" : { + "key": "026bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce214" + }, + "output": false + }, + { + "description" : "returns false if an uncompressed public key is given", + "input" : { + "key": "043752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553" + }, + "output": false + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-public-key.json b/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-public-key.json new file mode 100644 index 000000000..b031c274b --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/secp256k1/validate-public-key.json @@ -0,0 +1,33 @@ +{ + "description" : "Secp256k1 validatePublicKey test vectors", + "vectors" : [ + { + "description" : "returns true for valid compressed public keys", + "input" : { + "key": "026bcdccc644b309921d3b0c266183a20786650c1634d34e8dfa1ed74cd66ce214" + }, + "output": true + }, + { + "description" : "returns true for valid uncompressed public keys", + "input" : { + "key": "043752951274023296c8a74b0ffe42f82ff4b4d4bba4326477422703f761f59258c26a7465b9a77ac0c3f1cedb139c428b0b1fbb5516867b527636f3286f705553" + }, + "output": true + }, + { + "description" : "returns false for invalid public keys", + "input" : { + "key": "02fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" + }, + "output": false + }, + { + "description" : "returns false if a private key is given", + "input" : { + "key": "740ec69810de9ad1b8f298f1d2c0e6a52dd1e958dc2afc85764bec169c222e88" + }, + "output": false + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-private-key.json b/packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-private-key.json new file mode 100644 index 000000000..637001642 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-private-key.json @@ -0,0 +1,44 @@ +{ + "description" : "X25519 bytesToPrivateKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected private key", + "input" : { + "privateKeyBytes": "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475" + }, + "output": { + "crv" : "X25519", + "d" : "yKnVqRCRrYUcZosHNsHJoCk2wNOtYmcIWAiAR7oFdHU", + "kid" : "tzkgHF2KJ5d1ir5OaJi9VdNtwKrvnDUPUagtedeyyNE", + "kty" : "OKP", + "x" : "X2S0HM6Kaz1qOHYwiPYVpJd9QiKIrkK0mrOlfi_Nb20" + } + }, + { + "description" : "converts wycheproof vector 2 to the expected private key", + "input" : { + "privateKeyBytes": "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958" + }, + "output": { + "crv" : "X25519", + "d" : "2F2MBhpQgErEiK13SscWw_W6cUsnEuBISRN5pQAhGVg", + "kid" : "0FSNmN_cVErHxsnoRtFHANkAQyklGgA7crsE-HiFPwg", + "kty" : "OKP", + "x" : "qQMfm22ifgIOV-IEKfLitbg0jEkoIQbCpbI2WnI_DSA" + } + }, + { + "description" : "converts wycheproof vector 3 to the expected private key", + "input" : { + "privateKeyBytes": "c8b45bfd32e55325d9fd648cb302848039000b390e44d521e58aab3b29a6964b" + }, + "output": { + "crv" : "X25519", + "d" : "yLRb_TLlUyXZ_WSMswKEgDkACzkORNUh5YqrOymmlks", + "kid" : "OaJSSSZ5RBIGtsE8-ah1lWjcXrk5Fk4TYpuu_AgQKJs", + "kty" : "OKP", + "x" : "_s8qHPERyRymPqNATkQbqkB7-jpudS-30g-VRmkN00M" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-public-key.json b/packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-public-key.json new file mode 100644 index 000000000..380bdb65e --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/x25519/bytes-to-public-key.json @@ -0,0 +1,41 @@ +{ + "description" : "X25519 bytesToPublicKey test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected public key", + "input" : { + "publicKeyBytes": "5f64b41cce8a6b3d6a38763088f615a4977d422288ae42b49ab3a57e2fcd6f6d" + }, + "output": { + "crv" : "X25519", + "kid" : "tzkgHF2KJ5d1ir5OaJi9VdNtwKrvnDUPUagtedeyyNE", + "kty" : "OKP", + "x" : "X2S0HM6Kaz1qOHYwiPYVpJd9QiKIrkK0mrOlfi_Nb20" + } + }, + { + "description" : "converts wycheproof vector 2 to the expected public key", + "input" : { + "publicKeyBytes": "a9031f9b6da27e020e57e20429f2e2b5b8348c49282106c2a5b2365a723f0d20" + }, + "output": { + "crv" : "X25519", + "kid" : "0FSNmN_cVErHxsnoRtFHANkAQyklGgA7crsE-HiFPwg", + "kty" : "OKP", + "x" : "qQMfm22ifgIOV-IEKfLitbg0jEkoIQbCpbI2WnI_DSA" + } + }, + { + "description" : "converts wycheproof vector 3 to the expected public key", + "input" : { + "publicKeyBytes": "fecf2a1cf111c91ca63ea3404e441baa407bfa3a6e752fb7d20f9546690dd343" + }, + "output": { + "crv" : "X25519", + "kid" : "OaJSSSZ5RBIGtsE8-ah1lWjcXrk5Fk4TYpuu_AgQKJs", + "kty" : "OKP", + "x" : "_s8qHPERyRymPqNATkQbqkB7-jpudS-30g-VRmkN00M" + } + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/x25519/private-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/x25519/private-key-to-bytes.json new file mode 100644 index 000000000..d24abc274 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/x25519/private-key-to-bytes.json @@ -0,0 +1,44 @@ +{ + "description" : "X25519 privateKeyToBytes test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "X25519", + "d" : "yKnVqRCRrYUcZosHNsHJoCk2wNOtYmcIWAiAR7oFdHU", + "kid" : "tzkgHF2KJ5d1ir5OaJi9VdNtwKrvnDUPUagtedeyyNE", + "kty" : "OKP", + "x" : "X2S0HM6Kaz1qOHYwiPYVpJd9QiKIrkK0mrOlfi_Nb20" + } + }, + "output": "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475" + }, + { + "description" : "converts wycheproof vector 2 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "X25519", + "d" : "2F2MBhpQgErEiK13SscWw_W6cUsnEuBISRN5pQAhGVg", + "kid" : "0FSNmN_cVErHxsnoRtFHANkAQyklGgA7crsE-HiFPwg", + "kty" : "OKP", + "x" : "qQMfm22ifgIOV-IEKfLitbg0jEkoIQbCpbI2WnI_DSA" + } + }, + "output": "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958" + }, + { + "description" : "converts wycheproof vector 3 to the expected byte array", + "input" : { + "privateKey": { + "crv" : "X25519", + "d" : "yLRb_TLlUyXZ_WSMswKEgDkACzkORNUh5YqrOymmlks", + "kid" : "OaJSSSZ5RBIGtsE8-ah1lWjcXrk5Fk4TYpuu_AgQKJs", + "kty" : "OKP", + "x" : "_s8qHPERyRymPqNATkQbqkB7-jpudS-30g-VRmkN00M" + } + }, + "output": "c8b45bfd32e55325d9fd648cb302848039000b390e44d521e58aab3b29a6964b" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/fixtures/test-vectors/x25519/public-key-to-bytes.json b/packages/crypto/tests/fixtures/test-vectors/x25519/public-key-to-bytes.json new file mode 100644 index 000000000..f6c0fb5a4 --- /dev/null +++ b/packages/crypto/tests/fixtures/test-vectors/x25519/public-key-to-bytes.json @@ -0,0 +1,41 @@ +{ + "description" : "X25519 publicKeyToBytes test vectors", + "vectors" : [ + { + "description" : "converts wycheproof vector 1 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "X25519", + "kid" : "tzkgHF2KJ5d1ir5OaJi9VdNtwKrvnDUPUagtedeyyNE", + "kty" : "OKP", + "x" : "X2S0HM6Kaz1qOHYwiPYVpJd9QiKIrkK0mrOlfi_Nb20" + } + }, + "output": "5f64b41cce8a6b3d6a38763088f615a4977d422288ae42b49ab3a57e2fcd6f6d" + }, + { + "description" : "converts wycheproof vector 2 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "X25519", + "kid" : "0FSNmN_cVErHxsnoRtFHANkAQyklGgA7crsE-HiFPwg", + "kty" : "OKP", + "x" : "qQMfm22ifgIOV-IEKfLitbg0jEkoIQbCpbI2WnI_DSA" + } + }, + "output": "a9031f9b6da27e020e57e20429f2e2b5b8348c49282106c2a5b2365a723f0d20" + }, + { + "description" : "converts wycheproof vector 3 to the expected byte array", + "input" : { + "publicKey": { + "crv" : "X25519", + "kid" : "OaJSSSZ5RBIGtsE8-ah1lWjcXrk5Fk4TYpuu_AgQKJs", + "kty" : "OKP", + "x" : "_s8qHPERyRymPqNATkQbqkB7-jpudS-30g-VRmkN00M" + } + }, + "output": "fecf2a1cf111c91ca63ea3404e441baa407bfa3a6e752fb7d20f9546690dd343" + } + ] +} \ No newline at end of file diff --git a/packages/crypto/tests/jose.spec.ts b/packages/crypto/tests/jose.spec.ts index 46df2c703..4a608c316 100644 --- a/packages/crypto/tests/jose.spec.ts +++ b/packages/crypto/tests/jose.spec.ts @@ -1,153 +1,285 @@ import chai, { expect } from 'chai'; -import { Convert, MulticodecCode, MulticodecDefinition } from '@web5/common'; import chaiAsPromised from 'chai-as-promised'; +import { MulticodecCode, MulticodecDefinition } from '@web5/common'; -import type { JsonWebKey } from '../src/jose.js'; -import type { Web5Crypto } from '../src/types/web5-crypto.js'; +import type { JsonWebKey, PublicKeyJwk } from '../src/jose.js'; -import { CryptoKeyWithJwk, Jose } from '../src/jose.js'; +import { Jose } from '../src/jose.js'; import { - cryptoKeyToJwkTestVectors, - cryptoKeyPairToJsonWebKeyTestVectors, - joseToWebCryptoTestVectors, - keyToJwkWebCryptoTestVectors, - keyToJwkMulticodecTestVectors, - keyToJwkTestVectorsKeyMaterial, - joseToMulticodecTestVectors, jwkToThumbprintTestVectors, - jwkToCryptoKeyTestVectors, - jwkToKeyTestVectors, + joseToMulticodecTestVectors, jwkToMultibaseIdTestVectors, - keyToJwkWebCryptoWithNullKTYTestVectors, } from './fixtures/test-vectors/jose.js'; chai.use(chaiAsPromised); -describe('CryptoKeyWithJwk()', () => { - it('converts private CryptoKeys to JWK', async () => { - for (const vector of cryptoKeyPairToJsonWebKeyTestVectors) { - const privateKey = { - ...vector.cryptoKey.privateKey, - material: Convert.hex(vector.cryptoKey.privateKey.material).toUint8Array() - } as Web5Crypto.CryptoKey; - - const cryptoKey = new CryptoKeyWithJwk( - privateKey.algorithm, - privateKey.extractable, - privateKey.material, - privateKey.type, - privateKey.usages - ); - - const jsonWebKey = await cryptoKey.toJwk(); - - expect(jsonWebKey).to.deep.equal(vector.jsonWebKey.privateKeyJwk); - } - }); +describe('Jose', () => { + describe('isEcPrivateKeyJwk', () => { + it('returns true for a valid EC private key JWK', () => { + const validEcJwk = { + kty : 'EC', + crv : 'P-256', + x : 'base64url-encoded-x-value', + d : 'base64url-encoded-private-key' + }; + expect(Jose.isEcPrivateKeyJwk(validEcJwk)).to.be.true; + }); + + it('returns false for non-object inputs', () => { + expect(Jose.isEcPrivateKeyJwk(null)).to.be.false; + expect(Jose.isEcPrivateKeyJwk(undefined)).to.be.false; + expect(Jose.isEcPrivateKeyJwk(123)).to.be.false; + expect(Jose.isEcPrivateKeyJwk('string')).to.be.false; + expect(Jose.isEcPrivateKeyJwk([])).to.be.false; + }); + + it('returns false if any required property is missing', () => { + const missingKty = { crv: 'P-256', x: 'base64url-encoded-x-value', d: 'base64url-encoded-private-key' }; + const missingCrv = { kty: 'EC', x: 'base64url-encoded-x-value', d: 'base64url-encoded-private-key' }; + const missingX = { kty: 'EC', crv: 'P-256', d: 'base64url-encoded-private-key' }; + const missingD = { kty: 'EC', crv: 'P-256', x: 'base64url-encoded-x-value' }; + + expect(Jose.isEcPrivateKeyJwk(missingKty)).to.be.false; + expect(Jose.isEcPrivateKeyJwk(missingCrv)).to.be.false; + expect(Jose.isEcPrivateKeyJwk(missingX)).to.be.false; + expect(Jose.isEcPrivateKeyJwk(missingD)).to.be.false; + }); + + it('returns false if kty is not EC', () => { + const invalidKty = { kty: 'RSA', crv: 'P-256', x: 'base64url-encoded-x-value', d: 'base64url-encoded-private-key' }; + expect(Jose.isEcPrivateKeyJwk(invalidKty)).to.be.false; + }); + + it('returns false if any property is of incorrect type', () => { + const invalidDType = { kty: 'EC', crv: 'P-256', x: 'base64url-encoded-x-value', d: 123 }; + const invalidXType = { kty: 'EC', crv: 'P-256', x: 123, d: 'base64url-encoded-private-key' }; + + expect(Jose.isEcPrivateKeyJwk(invalidDType)).to.be.false; + expect(Jose.isEcPrivateKeyJwk(invalidXType)).to.be.false; + }); - it('converts public CryptoKeys to JWK', async () => { - for (const vector of cryptoKeyPairToJsonWebKeyTestVectors) { - const publicKey = { - ...vector.cryptoKey.publicKey, - material: Convert.hex(vector.cryptoKey.publicKey.material).toUint8Array() - } as Web5Crypto.CryptoKey; - - const cryptoKey = new CryptoKeyWithJwk( - publicKey.algorithm, - publicKey.extractable, - publicKey.material, - publicKey.type, - publicKey.usages - ); - - const jsonWebKey = await cryptoKey.toJwk(); - - expect(jsonWebKey).to.deep.equal(vector.jsonWebKey.publicKeyJwk); - } + it('returns true for valid EC JWK with extra properties', () => { + const validEcJwkExtra = { + kty : 'EC', + crv : 'P-256', + x : 'base64url-encoded-x-value', + d : 'base64url-encoded-private-key', + extra : 'extra-value' + }; + expect(Jose.isEcPrivateKeyJwk(validEcJwkExtra)).to.be.true; + }); }); - it('converts secret CryptoKeys to JWK', async () => { - for (const vector of cryptoKeyToJwkTestVectors) { - const secretKey = { - ...vector.cryptoKey, - material: Convert.hex(vector.cryptoKey.material).toUint8Array() - } as Web5Crypto.CryptoKey; - - const cryptoKey = new CryptoKeyWithJwk( - secretKey.algorithm, - secretKey.extractable, - secretKey.material, - secretKey.type, - secretKey.usages - ); - - const jsonWebKey = await cryptoKey.toJwk(); - - expect(jsonWebKey).to.deep.equal(vector.jsonWebKey); - } + describe('isEcPublicKeyJwk', () => { + it('returns true for a valid EC public key JWK', () => { + const validEcJwk = { + kty : 'EC', + crv : 'P-256', + x : 'base64url-encoded-x-value' + }; + expect(Jose.isEcPublicKeyJwk(validEcJwk)).to.be.true; + }); + + it('returns false for non-object inputs', () => { + expect(Jose.isEcPublicKeyJwk(null)).to.be.false; + expect(Jose.isEcPublicKeyJwk(undefined)).to.be.false; + expect(Jose.isEcPublicKeyJwk(123)).to.be.false; + expect(Jose.isEcPublicKeyJwk('string')).to.be.false; + expect(Jose.isEcPublicKeyJwk([])).to.be.false; + }); + + it('returns false if any required property is missing', () => { + const missingKty = { crv: 'P-256', x: 'base64url-encoded-x-value' }; + const missingCrv = { kty: 'EC', x: 'base64url-encoded-x-value' }; + const missingX = { kty: 'EC', crv: 'P-256' }; + + expect(Jose.isEcPublicKeyJwk(missingKty)).to.be.false; + expect(Jose.isEcPublicKeyJwk(missingCrv)).to.be.false; + expect(Jose.isEcPublicKeyJwk(missingX)).to.be.false; + }); + + it('returns false if kty is not EC', () => { + const invalidKty = { kty: 'RSA', crv: 'P-256', x: 'base64url-encoded-x-value' }; + expect(Jose.isEcPublicKeyJwk(invalidKty)).to.be.false; + }); + + it('returns false if any property is of incorrect type', () => { + const invalidXType = { kty: 'EC', crv: 'P-256', x: 123 }; + + expect(Jose.isEcPublicKeyJwk(invalidXType)).to.be.false; + }); + + it('returns false if the private key parameter \'d\' is present', () => { + const withDParam = { kty: 'EC', crv: 'P-256', x: 'base64url-encoded-x-value', d: 'base64url-encoded-d-value' }; + expect(Jose.isEcPublicKeyJwk(withDParam)).to.be.false; + }); + + it('returns true for valid EC public JWK with extra properties', () => { + const validEcJwkExtra = { + kty : 'EC', + crv : 'P-256', + x : 'base64url-encoded-x-value', + extra : 'extra-value' + }; + expect(Jose.isEcPublicKeyJwk(validEcJwkExtra)).to.be.true; + }); }); - it('converts public CryptoKeys with extractable=false', async () => { - for (const vector of cryptoKeyPairToJsonWebKeyTestVectors) { - const publicKey = { - ...vector.cryptoKey.publicKey, - material: Convert.hex(vector.cryptoKey.publicKey.material).toUint8Array() - } as Web5Crypto.CryptoKey; - - const cryptoKey = new CryptoKeyWithJwk( - publicKey.algorithm, - false, // override extractable to false - publicKey.material, - publicKey.type, - publicKey.usages - ); - - const jsonWebKey = await cryptoKey.toJwk(); - - expect(jsonWebKey).to.deep.equal({ ...vector.jsonWebKey.publicKeyJwk, ext: 'false' }); - } + describe('isOctPrivateKeyJwk()', () => { + it('returns true for a valid OCT private key JWK', () => { + const validOctJwk = { + kty : 'oct', + k : 'base64url-encoded-key' + }; + expect(Jose.isOctPrivateKeyJwk(validOctJwk)).to.be.true; + }); + + it('returns false for non-object inputs', () => { + expect(Jose.isOctPrivateKeyJwk(null)).to.be.false; + expect(Jose.isOctPrivateKeyJwk(undefined)).to.be.false; + expect(Jose.isOctPrivateKeyJwk(123)).to.be.false; + expect(Jose.isOctPrivateKeyJwk('string')).to.be.false; + expect(Jose.isOctPrivateKeyJwk([])).to.be.false; + }); + + it('returns false if any required property is missing', () => { + const missingKty = { k: 'base64url-encoded-key' }; + const missingK = { kty: 'oct' }; + + expect(Jose.isOctPrivateKeyJwk(missingKty)).to.be.false; + expect(Jose.isOctPrivateKeyJwk(missingK)).to.be.false; + }); + + it('returns false if kty is not oct', () => { + const invalidKty = { kty: 'RSA', k: 'base64url-encoded-key' }; + expect(Jose.isOctPrivateKeyJwk(invalidKty)).to.be.false; + }); + + it('returns false if any property is of incorrect type', () => { + const invalidKType = { kty: 'oct', k: 123 }; + + expect(Jose.isOctPrivateKeyJwk(invalidKType)).to.be.false; + }); + + it('returns true for valid OCT private JWK with extra properties', () => { + const validOctJwkExtra = { + kty : 'oct', + k : 'base64url-encoded-key', + extra : 'extra-value' + }; + expect(Jose.isOctPrivateKeyJwk(validOctJwkExtra)).to.be.true; + }); }); - it('throws an error with unsupported algorithms', async () => { - const cryptoKey = new CryptoKeyWithJwk( - { name: 'ECDSA', namedCurve: 'P-256' }, // algorithm identifier - false, // extractable - new Uint8Array(32), // material aka key material - 'private', // key type - ['sign', 'verify'] // key usages - ); - - await expect( - cryptoKey.toJwk() - ).to.eventually.be.rejectedWith(Error, 'Unsupported key to JWK conversion: P-256'); + describe('isOkpPrivateKeyJwk()', () => { + it('returns true for a valid OKP private key JWK', () => { + const validOkpJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : 'base64url-encoded-x-value', + d : 'base64url-encoded-private-key' + }; + expect(Jose.isOkpPrivateKeyJwk(validOkpJwk)).to.be.true; + }); + + it('returns false for non-object inputs', () => { + expect(Jose.isOkpPrivateKeyJwk(null)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk(undefined)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk(123)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk('string')).to.be.false; + expect(Jose.isOkpPrivateKeyJwk([])).to.be.false; + }); + + it('returns false if any required property is missing', () => { + const missingKty = { crv: 'Ed25519', x: 'base64url-encoded-x-value', d: 'base64url-encoded-private-key' }; + const missingCrv = { kty: 'OKP', x: 'base64url-encoded-x-value', d: 'base64url-encoded-private-key' }; + const missingX = { kty: 'OKP', crv: 'Ed25519', d: 'base64url-encoded-private-key' }; + const missingD = { kty: 'OKP', crv: 'Ed25519', x: 'base64url-encoded-x-value' }; + + expect(Jose.isOkpPrivateKeyJwk(missingKty)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk(missingCrv)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk(missingX)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk(missingD)).to.be.false; + }); + + it('returns false if kty is not OKP', () => { + const invalidKty = { kty: 'EC', crv: 'Ed25519', x: 'base64url-encoded-x-value', d: 'base64url-encoded-private-key' }; + expect(Jose.isOkpPrivateKeyJwk(invalidKty)).to.be.false; + }); + + it('returns false if any property is of incorrect type', () => { + const invalidDType = { kty: 'OKP', crv: 'Ed25519', x: 'base64url-encoded-x-value', d: 123 }; + const invalidXType = { kty: 'OKP', crv: 'Ed25519', x: 123, d: 'base64url-encoded-private-key' }; + + expect(Jose.isOkpPrivateKeyJwk(invalidDType)).to.be.false; + expect(Jose.isOkpPrivateKeyJwk(invalidXType)).to.be.false; + }); + + it('returns true for valid OKP private JWK with extra properties', () => { + const validOkpJwkExtra = { + kty : 'OKP', + crv : 'Ed25519', + x : 'base64url-encoded-x-value', + d : 'base64url-encoded-private-key', + extra : 'extra-value' + }; + expect(Jose.isOkpPrivateKeyJwk(validOkpJwkExtra)).to.be.true; + }); }); -}); -describe('Jose', () => { - describe('joseToWebCrypto()', () => { - it('translates algorithm format from JOSE to WebCrypto', () => { - let webCrypto: Web5Crypto.GenerateKeyOptions; - for (const vector of joseToWebCryptoTestVectors) { - webCrypto = Jose.joseToWebCrypto(vector.jose as JsonWebKey); - expect(webCrypto).to.deep.equal(vector.webCrypto); - } + + describe('isOkpPublicKeyJwk()', () => { + it('returns true for a valid OKP public key JWK', () => { + const validOkpJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : 'base64url-encoded-x-value' + }; + expect(Jose.isOkpPublicKeyJwk(validOkpJwk)).to.be.true; + }); + + it('returns false for non-object inputs', () => { + expect(Jose.isOkpPublicKeyJwk(null)).to.be.false; + expect(Jose.isOkpPublicKeyJwk(undefined)).to.be.false; + expect(Jose.isOkpPublicKeyJwk(123)).to.be.false; + expect(Jose.isOkpPublicKeyJwk('string')).to.be.false; + expect(Jose.isOkpPublicKeyJwk([])).to.be.false; }); - it('throws an error if required parameters are missing', () => { - expect( - () => Jose.joseToWebCrypto({}) - ).to.throw(TypeError, 'One or more parameters missing'); + it('returns false if any required property is missing', () => { + const missingKty = { crv: 'Ed25519', x: 'base64url-encoded-x-value' }; + const missingCrv = { kty: 'OKP', x: 'base64url-encoded-x-value' }; + const missingX = { kty: 'OKP', crv: 'Ed25519' }; + + expect(Jose.isOkpPublicKeyJwk(missingKty)).to.be.false; + expect(Jose.isOkpPublicKeyJwk(missingCrv)).to.be.false; + expect(Jose.isOkpPublicKeyJwk(missingX)).to.be.false; + }); + + it('returns false if kty is not OKP', () => { + const invalidKty = { kty: 'EC', crv: 'Ed25519', x: 'base64url-encoded-x-value' }; + expect(Jose.isOkpPublicKeyJwk(invalidKty)).to.be.false; }); - it('throws an error if an unknown JOSE algorithm is specified', () => { - expect( - () => Jose.joseToWebCrypto({ alg: 'non-existent' }) - ).to.throw(Error, `Unsupported JOSE to WebCrypto conversion: 'non-existent'`); + it('returns false if any property is of incorrect type', () => { + const invalidXType = { kty: 'OKP', crv: 'Ed25519', x: 123 }; + + expect(Jose.isOkpPublicKeyJwk(invalidXType)).to.be.false; + }); + + it(`returns false if the private key parameter 'd' is present`, () => { + const withDParam = { kty: 'OKP', crv: 'Ed25519', x: 'base64url-encoded-x-value', d: 'base64url-encoded-d-value' }; + expect(Jose.isOkpPublicKeyJwk(withDParam)).to.be.false; + }); - expect( - // @ts-expect-error because invalid algorithm was intentionally specified to trigger an error. - () => Jose.joseToWebCrypto({ crv: 'non-existent' }) - ).to.throw(Error, `Unsupported JOSE to WebCrypto conversion: 'non-existent'`); + it('returns true for valid OKP public JWK with extra properties', () => { + const validOkpJwkExtra = { + kty : 'OKP', + crv : 'Ed25519', + x : 'base64url-encoded-x-value', + extra : 'extra-value' + }; + expect(Jose.isOkpPublicKeyJwk(validOkpJwkExtra)).to.be.true; }); }); @@ -188,205 +320,137 @@ describe('Jose', () => { }); }); - describe('jwkToCryptoKey()', () => { + describe('publicKeyToMultibaseId()', () => { it('passes all test vectors', async () => { - let cryptoKey: Web5Crypto.CryptoKey; + let multibaseId: string; - for (const vector of jwkToCryptoKeyTestVectors) { - cryptoKey = await Jose.jwkToCryptoKey({ key: vector.jsonWebKey as JsonWebKey}); - expect(cryptoKey).to.deep.equal(vector.cryptoKey); + for (const vector of jwkToMultibaseIdTestVectors) { + multibaseId = await Jose.publicKeyToMultibaseId({ publicKey: vector.input as PublicKeyJwk}); + expect(multibaseId).to.equal(vector.output); } }); - it('throws an error when ext parameter is missing', async () => { + it('throws an error for an unsupported public key type', async () => { await expect( - Jose.jwkToCryptoKey({key: { - 'alg' : 'A256CTR', - 'key_ops' : ['encrypt', 'decrypt'], - 'k' : 'UQtIAS-rmWB-vgNgG4lPrnTS2tNvwDPKl9rs0L9ICnU', - 'kty' : 'oct', - }}) - ).to.eventually.be.rejectedWith(Error, `Conversion from JWK to CryptoKey failed. Required parameter missing: 'ext'`); + Jose.publicKeyToMultibaseId({ + publicKey: { + kty : 'RSA', + n : 'r0YDzIV4GPJ1wFb1Gftdd3C3VE6YeknVq1C7jGypq5WTTmX0yRDBqzL6mBR3_c-mKRuE5Z5VMGniA1lFnFmv8m0A2engKfALXHPJqoL6WzqN1SyjSM2aI6v8JVTj4H0RdYV9R4jxIB-zK5X-ZyL6CwHx-3dKZkCvZSEp8b-5I8c2Fz8E8Hl7qKkD_qEz6ZOmKVhJLGiEag1qUQYJv2TcRdiyZfwwVsV3nI3IcVfMCTjDZTw2jI0YHJgLi7-MkP4DO7OJ4D4AFtL-7CkZ7V2xG0piBz4b02_-ZGnBZ5zHJxGoUZnTY6HX4V9bPQI_ME8qCjFXf-TcwCfDFcwMm70L2Q', + e : 'AQAB', + alg : 'RS256' + } + }) + ).to.eventually.be.rejectedWith(Error, `Unsupported public key type`); }); - it('throws an error when key_ops parameter is missing', async () => { + it('throws an error for an unsupported public key curve', async () => { await expect( - Jose.jwkToCryptoKey({key: { - 'alg' : 'A256CTR', - 'ext' : 'true', - 'k' : 'UQtIAS-rmWB-vgNgG4lPrnTS2tNvwDPKl9rs0L9ICnU', - 'kty' : 'oct', - }}) - ).to.eventually.be.rejectedWith(Error, `Conversion from JWK to CryptoKey failed. Required parameter missing: 'key_ops'`); + Jose.publicKeyToMultibaseId({ + publicKey: { + kty : 'EC', + crv : 'P-256', + x : 'SVqB4JcUD6lsfvqMr-OKUNUphdNn64Eay60978ZlL74', + y : 'lf0u0pMj4lGAzZix5u4Cm5CMQIgMNpkwy163wtKYVKI' + } + }) + ).to.eventually.be.rejectedWith(Error, `Unsupported public key curve`); }); }); - describe('jwkToKey()', () => { - it('converts JWK into Jose parameters', async () => { - let jwk: { keyMaterial: Uint8Array; keyType: Web5Crypto.KeyType }; - - for (const vector of jwkToKeyTestVectors) { - jwk = await Jose.jwkToKey({ key: vector.input as JsonWebKey}); - const hexKeyMaterial = Convert.uint8Array(jwk.keyMaterial).toHex(); - - expect({...jwk, keyMaterial: hexKeyMaterial}).to.deep.equal(vector.output); - } + describe('multicodecToJose()', () => { + it('converts ed25519 public key multicodec to JWK', async () => { + const result = await Jose.multicodecToJose({ name: 'ed25519-pub' }); + expect(result).to.deep.equal({ + crv : 'Ed25519', + kty : 'OKP', + x : '' // x value would be populated with actual key material in real use + }); }); - it('throws an error if unsupported JOSE has been passed', async () => { - await expect( - // @ts-expect-error because parameters are intentionally omitted to trigger an error. - Jose.jwkToKey({ key: { alg: 'HS256', kty: 'oct' }}) - ).to.eventually.be.rejectedWith(Error, `Jose: Unknown JSON Web Key format.`); + it('converts ed25519 private key multicodec to JWK', async () => { + const result = await Jose.multicodecToJose({ name: 'ed25519-priv' }); + expect(result).to.deep.equal({ + crv : 'Ed25519', + kty : 'OKP', + x : '', // x value would be populated with actual key material in real use + d : '' // d value would be populated with actual key material in real use + }); }); - }); - describe('jwkToMultibaseId()', () => { - it('passes all test vectors', async () => { - let multibaseId: string; - - for (const vector of jwkToMultibaseIdTestVectors) { - multibaseId = await Jose.jwkToMultibaseId({ key: vector.input as JsonWebKey}); - expect(multibaseId).to.equal(vector.output); - } - }); - - // it('throws an error when ext parameter is missing', async () => { - // await expect( - // Jose.jwkToCryptoKey({key: { - // 'alg' : 'A256CTR', - // 'key_ops' : ['encrypt', 'decrypt'], - // 'k' : 'UQtIAS-rmWB-vgNgG4lPrnTS2tNvwDPKl9rs0L9ICnU', - // 'kty' : 'oct', - // }}) - // ).to.eventually.be.rejectedWith(Error, `Conversion from JWK to CryptoKey failed. Required parameter missing: 'ext'`); - // }); - }); - - describe('keyToJwk()', () => { - it('converts key with Jose parameters (from WebCrypto) into JWK', async () => { - let jwkParams: Partial; - const keyMaterial = Convert.hex(keyToJwkTestVectorsKeyMaterial).toUint8Array(); - - for (const vector of keyToJwkWebCryptoTestVectors) { - jwkParams = Jose.webCryptoToJose(vector.input); - const jwk = await Jose.keyToJwk({ keyMaterial, keyType: 'public', ...jwkParams }); - expect(jwk).to.deep.equal(vector.output); - } + it('converts secp256k1 public key multicodec to JWK', async () => { + const result = await Jose.multicodecToJose({ name: 'secp256k1-pub' }); + expect(result).to.deep.equal({ + crv : 'secp256k1', + kty : 'EC', + x : '', // x value would be populated with actual key material in real use + y : '' // y value would be populated with actual key material in real use + }); }); - it('converts key with Jose parameters (from Multicodec) into JWK', async () => { - let jwkParams: Partial; - const keyMaterial = Convert.hex(keyToJwkTestVectorsKeyMaterial).toUint8Array(); - - for (const vector of keyToJwkMulticodecTestVectors) { - jwkParams = await Jose.multicodecToJose({ name: vector.input }); - const keyType = vector.input.includes('priv') ? 'private' : 'public'; - const jwk = await Jose.keyToJwk({ keyMaterial, keyType, ...jwkParams }); - expect(jwk).to.deep.equal(vector.output); - } + it('converts secp256k1 private key multicodec to JWK', async () => { + const result = await Jose.multicodecToJose({ name: 'secp256k1-priv' }); + expect(result).to.deep.equal({ + crv : 'secp256k1', + kty : 'EC', + x : '', // x value would be populated with actual key material in real use + y : '', // y value would be populated with actual key material in real use + d : '' // d value would be populated with actual key material in real use + }); }); - it('coverts when kty equals to null', async () => { - let jwkParams: Partial; - const keyMaterial = Convert.hex(keyToJwkTestVectorsKeyMaterial).toUint8Array(); - - for (const vector of keyToJwkWebCryptoWithNullKTYTestVectors) { - jwkParams = Jose.webCryptoToJose(vector.input); - // @ts-expect-error because parameters are intentionally omitted to trigger an error. - const jwk = await Jose.keyToJwk({ keyMaterial, keyType: 'public', ...jwkParams, kty: null }); - expect(jwk).to.deep.equal(vector.output); - } + it('converts x25519 public key multicodec to JWK', async () => { + const result = await Jose.multicodecToJose({ name: 'x25519-pub' }); + expect(result).to.deep.equal({ + crv : 'X25519', + kty : 'OKP', + x : '' // x value would be populated with actual key material in real use + }); }); - it('throws an error for wrong arguments', async () => { - await expect( - Jose.multicodecToJose({ name: 'intentionally-wrong-name', code: 12345 }) - ).to.eventually.be.rejectedWith(Error, `Either 'name' or 'code' must be defined, but not both.`); + it('converts x25519 private key multicodec to JWK', async () => { + const result = await Jose.multicodecToJose({ name: 'x25519-priv' }); + expect(result).to.deep.equal({ + crv : 'X25519', + kty : 'OKP', + x : '', // x value would be populated with actual key material in real use + d : '' // d value would be populated with actual key material in real use + }); }); - it('handles undefined name', async () => { - const jwkParams = await Jose.multicodecToJose({ name: undefined, code: 0xed }); - expect(jwkParams).to.deep.equal({ alg: 'EdDSA', crv: 'Ed25519', kty: 'OKP', x: '' }); - }); - - it('throws an error for unsupported multicodec conversion', async () => { - await expect( - Jose.multicodecToJose({ name: 'intentionally-wrong-name' }) - ).to.eventually.be.rejectedWith(Error, `Unsupported Multicodec to JOSE conversion: 'intentionally-wrong-name'`); - }); - - it('throws an error for unsupported conversion', async () => { - let jwkParams: Partial; - const testVectors = [ - { namedCurve: 'Ed448', name: 'EdDSA' }, - { namedCurve: 'P-256', name: 'ECDSA' }, - { namedCurve: 'P-384', name: 'ECDSA' }, - { namedCurve: 'P-521', name: 'ECDSA' } - ]; - const keyMaterial = new Uint8Array(32); - for (const vector of testVectors) { - jwkParams = Jose.webCryptoToJose(vector); - await expect( - Jose.keyToJwk({ keyMaterial, keyType: 'public', ...jwkParams }) - ).to.eventually.be.rejectedWith(Error, 'Unsupported key to JWK conversion'); + it('throws an error when name is undefined and code is not provided', async () => { + try { + await Jose.multicodecToJose({}); + expect.fail('Should have thrown an error for undefined name and code'); + } catch (e: any) { + expect(e.message).to.equal('Either \'name\' or \'code\' must be defined, but not both.'); } }); - }); - describe('webCryptoToJose()', () => { - it('translates algorithm format from WebCrypto to JOSE', () => { - let jose: Partial; - for (const vector of joseToWebCryptoTestVectors) { - jose = Jose.webCryptoToJose(vector.webCrypto); - expect(jose).to.deep.equal(vector.jose); + it('throws an error when both name and code are provided', async () => { + try { + await Jose.multicodecToJose({ name: 'ed25519-pub', code: 0xed }); + expect.fail('Should have thrown an error for both name and code being defined'); + } catch (e: any) { + expect(e.message).to.equal('Either \'name\' or \'code\' must be defined, but not both.'); } }); - it('throws an error if required parameters are missing', () => { - expect( - // @ts-expect-error because parameters are intentionally omitted to trigger an error. - () => Jose.webCryptoToJose({}) - ).to.throw(TypeError, 'One or more parameters missing'); - }); - - it('throws an error if an unknown WebCrypto algorithm is specified', () => { - expect( - () => Jose.webCryptoToJose({ name: 'non-existent', namedCurve: 'non-existent' }) - ).to.throw(Error, `Unsupported WebCrypto to JOSE conversion: 'non-existent:non-existent'`); - - expect( - () => Jose.webCryptoToJose({ name: 'non-existent', length: 64 }) - ).to.throw(Error, `Unsupported WebCrypto to JOSE conversion: 'non-existent:64'`); - - expect( - () => Jose.webCryptoToJose({ name: 'non-existent', hash: { name: 'SHA-1' } }) - ).to.throw(Error, `Unsupported WebCrypto to JOSE conversion: 'non-existent:SHA-1'`); + it('throws an error for unsupported multicodec name', async () => { + try { + await Jose.multicodecToJose({ name: 'unsupported-key-type' }); + expect.fail('Should have thrown an error for unsupported multicodec name'); + } catch (e: any) { + expect(e.message).to.include('Unsupported Multicodec to JOSE conversion'); + } }); - }); - - describe('cryptoKeyToJwkPair()', () => { - it('converts CryptoKeys to JWK Pair', async () => { - for (const vector of cryptoKeyPairToJsonWebKeyTestVectors) { - const privateKey = { - ...vector.cryptoKey.privateKey, - material: Convert.hex( - vector.cryptoKey.privateKey.material - ).toUint8Array(), - } as Web5Crypto.CryptoKey; - const publicKey = { - ...vector.cryptoKey.publicKey, - material: Convert.hex( - vector.cryptoKey.publicKey.material - ).toUint8Array(), - } as Web5Crypto.CryptoKey; - - const jwkKeyPair = await Jose.cryptoKeyToJwkPair({ - keyPair: { publicKey, privateKey }, - }); - expect(jwkKeyPair).to.deep.equal(vector.jsonWebKey); + it('throws an error for unsupported multicodec code', async () => { + try { + await Jose.multicodecToJose({ code: 0x9999 }); + expect.fail('Should have thrown an error for unsupported multicodec code'); + } catch (e: any) { + expect(e.message).to.include('Unsupported multicodec'); } }); }); -}); +}); \ No newline at end of file diff --git a/packages/crypto/tests/tsconfig.json b/packages/crypto/tests/tsconfig.json index 7c6d2c8e7..0cdb42f92 100644 --- a/packages/crypto/tests/tsconfig.json +++ b/packages/crypto/tests/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "compiled", "declarationDir": "compiled/types", - "sourceMap": true, + "resolveJsonModule": true, + "sourceMap": true }, "include": [ "../src", diff --git a/packages/crypto/tests/utils.spec.ts b/packages/crypto/tests/utils.spec.ts index be88d1246..50a27e93f 100644 --- a/packages/crypto/tests/utils.spec.ts +++ b/packages/crypto/tests/utils.spec.ts @@ -1,10 +1,8 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { CryptoKey } from '../src/algorithms-api/crypto-key.js'; import { randomUuid, - isCryptoKeyPair, keyToMultibaseId, multibaseIdToKey, checkValidProperty, @@ -54,42 +52,6 @@ describe('Crypto Utils', () => { }); }); - describe('isCryptoKeyPair()', () => { - it('returns true with a CryptoKeyPair object', () => { - const publicKey = new CryptoKey( - { name: 'EdDSA', namedCurve: 'Ed25519' }, - true, - new Uint8Array(32), - 'public', - ['verify'] - ); - const privateKey = new CryptoKey( - { name: 'EdDSA', namedCurve: 'Ed25519' }, - true, - new Uint8Array(32), - 'private', - ['sign'] - ); - const validCryptoKeyPair = { publicKey, privateKey }; - - const result = isCryptoKeyPair(validCryptoKeyPair); - expect(result).to.be.true; - }); - - it('returns false for a CryptoKey', () => { - const cryptoKey = new CryptoKey( - { name: 'EdDSA', namedCurve: 'Ed25519' }, - true, - new Uint8Array(32), - 'secret', - ['decrypt', 'encrypt'] - ); - - const result = isCryptoKeyPair(cryptoKey); - expect(result).to.be.false; - }); - }); - describe('isWebCryptoSupported()', () => { afterEach(() => { // Restore the original state after each test diff --git a/packages/identity-agent/package.json b/packages/identity-agent/package.json index a61cbb5e7..142db6e71 100644 --- a/packages/identity-agent/package.json +++ b/packages/identity-agent/package.json @@ -68,7 +68,9 @@ }, "dependencies": { "@web5/agent": "0.2.4", - "@web5/api": "0.8.3" + "@web5/common": "0.2.1", + "@web5/crypto": "0.2.2", + "@web5/dids": "0.2.2" }, "devDependencies": { "@playwright/test": "1.36.2",