From e71679b79972070cc39dd991bb7c99ddf52645d1 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Tue, 25 Jun 2024 13:55:11 +0200 Subject: [PATCH] feat: remove storage Signed-off-by: Berend Sliedrecht --- package.json | 1 + pnpm-lock.yaml | 8 ++ src/CryptoKey.ts | 209 +++++++++++++++++++++++++++++++++++++++++ src/EcdsaProvider.ts | 102 +++++--------------- src/Ed25519Provider.ts | 32 +++++-- src/askar.ts | 175 +++------------------------------- src/index.ts | 2 - src/storage.ts | 52 ---------- src/types.ts | 15 ++- tests/x509.test.ts | 2 +- 10 files changed, 292 insertions(+), 306 deletions(-) create mode 100644 src/CryptoKey.ts delete mode 100644 src/storage.ts diff --git a/package.json b/package.json index 3892363..e8fab5c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", + "dtor": "^0.1.2", "webcrypto-core": "^1.8.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbf6a8a..da773bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@peculiar/asn1-x509': specifier: ^2.3.8 version: 2.3.8 + dtor: + specifier: ^0.1.2 + version: 0.1.2 webcrypto-core: specifier: ^1.8.0 version: 1.8.0 @@ -765,6 +768,9 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} + dtor@0.1.2: + resolution: {integrity: sha512-hdDEJIR5VfdE2VuCxnaWi/7lNOUTkISvDS5gsbQtZ0jfPpTeX4ihd/+8mJG5Su/6Y+GLPAMLrJK0MS4OVjKeAw==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2795,6 +2801,8 @@ snapshots: dependencies: is-obj: 2.0.0 + dtor@0.1.2: {} + eastasianwidth@0.2.0: {} emoji-regex@10.3.0: {} diff --git a/src/CryptoKey.ts b/src/CryptoKey.ts new file mode 100644 index 0000000..f5c7314 --- /dev/null +++ b/src/CryptoKey.ts @@ -0,0 +1,209 @@ +import * as core from 'webcrypto-core' + +import { Key as AskarKey, Jwk } from '@hyperledger/aries-askar-shared' +import type { EcdsaParams, JsonWebKey, KeyAlgorithm, KeyFormat, KeyImportParams, KeyType, KeyUsage } from './types' +import { askarAlgorithmToCryptoAlgorithm, cryptoAlgorithmToAskarAlgorithm } from './askar' +import { AsnConvert, AsnParser } from '@peculiar/asn1-schema' +import { ecdsaWithSHA256 } from '@peculiar/asn1-ecc' +import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509' + +export class AskarCryptoKey extends core.CryptoKey { + public askarKey: AskarKey + + public constructor({ + askarKey, + algorithm, + extractable = false, + usages, + type, + }: { + askarKey: AskarKey + extractable?: boolean + usages: Array + type?: KeyType + algorithm: KeyAlgorithm + }) { + super() + this.askarKey = askarKey + this.extractable = extractable + this.type = type + this.usages = usages + this.algorithm = algorithm + } + + public [Symbol.dispose]() { + this.askarKey.handle.free() + } + + public sign(algorithm: EcdsaParams, data: ArrayBuffer) { + if (algorithm.hash && algorithm.hash.name !== 'SHA-256') { + throw new Error(`Invalid hashing algorithm. Expected: 'SHA-256', received: ${algorithm.hash.name}`) + } + return this.askarKey.signMessage({ message: new Uint8Array(data) }) + } + + public verify(algorithm: EcdsaParams, signature: ArrayBuffer, data: ArrayBuffer) { + if (algorithm.hash && algorithm.hash.name !== 'SHA-256') { + throw new Error(`Invalid hashing algorithm. Expected: 'SHA-256', received: ${algorithm.hash.name}`) + } + + return this.askarKey.verifySignature({ + message: new Uint8Array(data), + signature: new Uint8Array(signature), + }) + } + + public get publicBytes() { + return this.askarKey.publicBytes + } + + /** + * + * @todo - Deal with key format + * + */ + public static fromPublicBytes( + algorithm: KeyImportParams, + keyData: Uint8Array, + _format: KeyFormat, + extractable: boolean, + keyUsages: Array + ) { + const publicKey = AskarKey.fromPublicBytes({ + algorithm: cryptoAlgorithmToAskarAlgorithm(algorithm), + publicKey: keyData, + }) + + return new AskarCryptoKey({ + askarKey: publicKey, + type: 'public', + algorithm, + usages: keyUsages, + extractable, + }) + } + + /** + * + * @todo - Deal with key format + * + */ + public static fromSecret( + algorithm: KeyImportParams, + keyData: Uint8Array, + _format: KeyFormat, + extractable: boolean, + keyUsages: Array + ) { + const publicKey = AskarKey.fromSecretBytes({ + algorithm: cryptoAlgorithmToAskarAlgorithm(algorithm), + secretKey: keyData, + }) + + return new AskarCryptoKey({ + askarKey: publicKey, + type: 'private', + algorithm, + usages: keyUsages, + extractable, + }) + } + + public toJwk() { + if (this.type === 'public') return this.askarKey.jwkPublic + if (this.type === 'private' || this.type === 'secret') { + return this.askarKey.jwkSecret + } + } + + public static fromJwk(keyData: JsonWebKey, keyUsages: Array, extractable: boolean) { + const key = AskarKey.fromJwk({ jwk: new Jwk(keyData) }) + + let type: KeyType = 'public' + try { + key.secretBytes + type = 'private' + } catch {} + + return new AskarCryptoKey({ + askarKey: key, + extractable, + usages: keyUsages, + type, + algorithm: askarAlgorithmToCryptoAlgorithm(key.algorithm), + }) + } + + public static override create( + algorithm: KeyAlgorithm, + type: KeyType, + extractable: boolean, + usages: core.KeyUsages + ): T { + return new AskarCryptoKey({ + askarKey: AskarKey.generate(cryptoAlgorithmToAskarAlgorithm(algorithm)), + algorithm, + extractable, + usages, + type, + }) as unknown as T + } + + public exportKey(format: KeyFormat): JsonWebKey | ArrayBuffer { + switch (format.toLowerCase()) { + case 'spki': { + const publicKeyInfo = new SubjectPublicKeyInfo({ + algorithm: ecdsaWithSHA256, + subjectPublicKey: this.publicBytes.buffer, + }) + + const derEncoded = AsnConvert.serialize(publicKeyInfo) + return derEncoded + } + case 'jwk': + return this.toJwk() as JsonWebKey + case 'raw': + // TODO: likely incorrect + return this.publicBytes.buffer + default: + throw new Error(`Not supported format: ${format}`) + } + } + + public static importKey( + format: KeyFormat, + keyData: JsonWebKey | ArrayBuffer, + algorithm: KeyAlgorithm, + extractable: boolean, + keyUsages: Array + ) { + if (format !== 'jwk' && ArrayBuffer.isView(keyData)) { + throw new core.OperationError('non-jwk formats can only be used with an ArrayBuffer') + } + + switch (format.toLowerCase()) { + case 'jwk': + return AskarCryptoKey.fromJwk(keyData as JsonWebKey, keyUsages, extractable) + case 'spki': { + const keyInfo = AsnParser.parse(new Uint8Array(keyData as ArrayBuffer), core.asn1.PublicKeyInfo) + + return AskarCryptoKey.fromPublicBytes( + algorithm, + new Uint8Array(keyInfo.publicKey), + format, + extractable, + keyUsages + ) + } + default: + throw new core.OperationError( + `Only format 'jwt' and 'spki' are supported for importing keys. Received: ${format}` + ) + } + } +} + +export const assertIsAskarCryptoKey = (askarKey: core.CryptoKey): AskarCryptoKey => { + if (askarKey instanceof AskarCryptoKey) return askarKey + throw new Error('key is not an instance of AskarCryptoKey') +} diff --git a/src/EcdsaProvider.ts b/src/EcdsaProvider.ts index 486900c..28623e5 100644 --- a/src/EcdsaProvider.ts +++ b/src/EcdsaProvider.ts @@ -1,16 +1,4 @@ -import { AsnConvert, AsnParser } from '@peculiar/asn1-schema' -import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509' -import { ecdsaWithSHA256 } from '@peculiar/asn1-ecc' import * as core from 'webcrypto-core' -import { - askarExportKeyToJwk, - askarKeyGenerate, - askarKeyGetPublicBytes, - askarKeySign, - askarKeyVerify, - askarKeyFromJwk, - askarKeyFromPublicBytes, -} from './askar' import type { CryptoKeyPair, EcKeyGenParams, @@ -20,31 +8,22 @@ import type { KeyFormat, KeyUsage, } from './types' +import { AskarCryptoKey, assertIsAskarCryptoKey } from './CryptoKey' export class EcdsaProvider extends core.EcdsaProvider { - public async onSign(algorithm: EcdsaParams, key: core.CryptoKey, data: ArrayBuffer): Promise { - if (algorithm.hash.name !== 'SHA-256') { - throw new Error(`Invalid hashing algorithm. Expected: 'SHA-256', received: ${algorithm.hash.name}`) - } - - const signature = askarKeySign({ key, data }) - - return signature + public async onSign(algorithm: EcdsaParams, key: AskarCryptoKey, data: ArrayBuffer): Promise { + assertIsAskarCryptoKey(key) + return key.sign(algorithm, data) } public async onVerify( algorithm: EcdsaParams, - key: core.CryptoKey, + key: AskarCryptoKey, signature: ArrayBuffer, data: ArrayBuffer ): Promise { - if (algorithm.hash.name !== 'SHA-256') { - throw new Error(`Invalid hashing algorithm. Expected: 'SHA-256', received: ${algorithm.hash.name}`) - } - - const isValid = askarKeyVerify({ key, data, signature }) - - return isValid + assertIsAskarCryptoKey(key) + return key.verify(algorithm, signature, data) } public async onGenerateKey( @@ -52,29 +31,24 @@ export class EcdsaProvider extends core.EcdsaProvider { extractable: boolean, keyUsages: KeyUsage[] ): Promise { - const key = askarKeyGenerate({ algorithm, extractable, keyUsages }) - - return key + const privateKey = AskarCryptoKey.create(algorithm, 'private', extractable, keyUsages) + const publicKey = new AskarCryptoKey({ + askarKey: privateKey.askarKey, + type: 'public', + usages: keyUsages, + algorithm, + extractable, + }) + + return { + publicKey, + privateKey, + } } - public async onExportKey(format: KeyFormat, key: core.CryptoKey): Promise { - switch (format.toLowerCase()) { - case 'spki': { - const publicKeyInfo = new SubjectPublicKeyInfo({ - algorithm: ecdsaWithSHA256, - subjectPublicKey: askarKeyGetPublicBytes(key).buffer, - }) - - const derEncoded = AsnConvert.serialize(publicKeyInfo) - return derEncoded - } - case 'jwk': - return askarExportKeyToJwk(key) - case 'raw': - return askarKeyGetPublicBytes(key).buffer - default: - throw new Error(`Not supported format: ${format}`) - } + public async onExportKey(format: KeyFormat, key: AskarCryptoKey): Promise { + assertIsAskarCryptoKey(key) + return key.exportKey(format) } public async onImportKey( @@ -83,33 +57,7 @@ export class EcdsaProvider extends core.EcdsaProvider { algorithm: EcKeyImportParams, extractable: boolean, keyUsages: KeyUsage[] - ): Promise { - if (format !== 'jwk' && ArrayBuffer.isView(keyData)) { - throw new core.OperationError('non-jwk formats can only be used with an ArrayBuffer') - } - - switch (format.toLowerCase()) { - case 'jwk': - return askarKeyFromJwk({ - extractable, - keyUsages, - keyData: keyData as JsonWebKey, - }) - case 'spki': { - const keyInfo = AsnParser.parse(new Uint8Array(keyData as ArrayBuffer), core.asn1.PublicKeyInfo) - - return askarKeyFromPublicBytes({ - format, - keyData: new Uint8Array(keyInfo.publicKey), - keyUsages, - extractable, - algorithm, - }) - } - default: - throw new core.OperationError( - `Only format 'jwt' and 'spki' are supported for importing keys. Received: ${format}` - ) - } + ): Promise { + return AskarCryptoKey.importKey(format, keyData, algorithm, extractable, keyUsages) } } diff --git a/src/Ed25519Provider.ts b/src/Ed25519Provider.ts index 9c1960c..c0296ae 100644 --- a/src/Ed25519Provider.ts +++ b/src/Ed25519Provider.ts @@ -1,19 +1,21 @@ import * as core from 'webcrypto-core' -import { askarKeyGenerate, askarKeySign, askarKeyVerify } from './askar' -import type { CryptoKeyPair, EcKeyGenParams, KeyUsage } from './types' +import type { CryptoKeyPair, EcKeyGenParams, KeySignParams, KeyUsage } from './types' +import { AskarCryptoKey, assertIsAskarCryptoKey } from './CryptoKey' export class Ed25519Provider extends core.Ed25519Provider { - public async onSign(_algorithm: string, key: core.CryptoKey, data: ArrayBuffer): Promise { - return askarKeySign({ key, data }) + public async onSign(algorithm: KeySignParams, key: AskarCryptoKey, data: ArrayBuffer): Promise { + assertIsAskarCryptoKey(key) + return key.sign(algorithm, data) } public async onVerify( - _algorithm: string, - key: core.CryptoKey, + algorithm: KeySignParams, + key: AskarCryptoKey, signature: ArrayBuffer, data: ArrayBuffer ): Promise { - return askarKeyVerify({ key, data, signature }) + assertIsAskarCryptoKey(key) + return key.verify(algorithm, signature, data) } public async onGenerateKey( @@ -21,6 +23,20 @@ export class Ed25519Provider extends core.Ed25519Provider { extractable: boolean, keyUsages: KeyUsage[] ): Promise { - return askarKeyGenerate({ algorithm, keyUsages, extractable }) + const privateKey = AskarCryptoKey.create(algorithm, 'private', extractable, keyUsages) + + // Create a public key from the private as internally they refer to the same key + const publicKey = new AskarCryptoKey({ + askarKey: privateKey.askarKey, + extractable, + algorithm, + usages: keyUsages, + type: 'public', + }) + + return { + publicKey, + privateKey, + } } } diff --git a/src/askar.ts b/src/askar.ts index 232a6f6..c850c3b 100644 --- a/src/askar.ts +++ b/src/askar.ts @@ -1,140 +1,8 @@ -import { CryptoBox, Jwk, Key, KeyAlgs } from '@hyperledger/aries-askar-shared' -import type * as core from 'webcrypto-core' -import { getCryptoKey, setCryptoKey } from './storage' -import type { EcKeyGenParams, EcKeyImportParams, JsonWebKey, KeyFormat, KeyUsage } from './types' +import { CryptoBox, KeyAlgs } from '@hyperledger/aries-askar-shared' +import type { KeyAlgorithm } from './types' const CBOX_NONCE_LENGTH = 24 -export const askarKeySign = ({ - key, - data, -}: { - key: core.CryptoKey - data: ArrayBuffer -}) => { - const internalKey = getCryptoKey(key) - const signature = internalKey.signMessage({ - message: new Uint8Array(data), - }) - - return signature -} - -export const askarKeyVerify = ({ - key, - data, - signature, -}: { - key: core.CryptoKey - data: ArrayBuffer - signature: ArrayBuffer -}) => { - const internalKey = getCryptoKey(key) - - const isVerified = internalKey.verifySignature({ - message: new Uint8Array(data), - signature: new Uint8Array(signature), - }) - - return isVerified -} - -export const askarKeyGenerate = ({ - extractable, - algorithm, - keyUsages, -}: { - algorithm: EcKeyGenParams - extractable: boolean - keyUsages: KeyUsage[] -}) => { - const key = Key.generate(cryptoAlgorithmToAskarAlgorithm(algorithm)) - - const publicKey = setCryptoKey({ - askarKey: key, - extractable, - // Filter out properties that are not possible for the public key - keyUsages: keyUsages.filter((u) => u !== 'sign'), - keyType: 'public', - }) - - const secretKey = setCryptoKey({ - askarKey: key, - keyType: 'private', - keyUsages, - extractable, - }) - - return { publicKey, privateKey: secretKey } -} - -export const askarKeyGetPublicBytes = (key: core.CryptoKey) => { - const cKey = getCryptoKey(key) - - return cKey.publicBytes -} - -export const askarKeyFromPublicBytes = ({ - algorithm, - keyData, - extractable, - keyUsages, -}: { - algorithm: EcKeyImportParams - keyData: Uint8Array - format: KeyFormat - extractable: boolean - keyUsages: KeyUsage[] -}) => { - const publicKey = Key.fromPublicBytes({ - algorithm: cryptoAlgorithmToAskarAlgorithm(algorithm), - publicKey: keyData, - }) - - return setCryptoKey({ - askarKey: publicKey, - extractable, - keyUsages, - keyType: 'public', - }) -} - -export const askarKeyFromSecretBytes = ({ - algorithm, - keyData, - extractable, - keyUsages, -}: { - algorithm: EcKeyImportParams - keyData: Uint8Array - format: KeyFormat - extractable: boolean - keyUsages: KeyUsage[] -}) => { - const privateKey = Key.fromSecretBytes({ - algorithm: cryptoAlgorithmToAskarAlgorithm(algorithm), - secretKey: keyData, - }) - - return setCryptoKey({ - askarKey: privateKey, - extractable, - keyUsages, - keyType: 'private', - }) -} - -export const askarExportKeyToJwk = (key: core.CryptoKey) => { - const askarKey = getCryptoKey(key) - - if (key.type === 'public') return askarKey.jwkPublic - if (key.type === 'private' || key.type === 'secret') { - return askarKey.jwkSecret - } - - throw new Error(`key.type '${key.type}' is not a string of 'public'/'private'/'secret'`) -} - export const askarGetRandomValues = (buffer: Uint8Array): Uint8Array => { const genCount = Math.ceil(buffer.length / CBOX_NONCE_LENGTH) const buf = new Uint8Array(genCount * CBOX_NONCE_LENGTH) @@ -146,36 +14,17 @@ export const askarGetRandomValues = (buffer: Uint8Array): Uint8Array => { return buffer } -export const askarKeyFromJwk = ({ - keyData, - keyUsages, - extractable, -}: { - keyData: JsonWebKey - keyUsages: KeyUsage[] - extractable: boolean -}) => { - const askarKey = Key.fromJwk({ jwk: new Jwk(keyData) }) - try { - askarKey.secretBytes - return setCryptoKey({ - askarKey, - keyUsages, - extractable, - keyType: 'private', - }) - } catch { - return setCryptoKey({ - askarKey, - keyUsages, - extractable, - keyType: 'public', - }) - } -} - // TODO: this needs a proper conversion -const cryptoAlgorithmToAskarAlgorithm = (algorithm: EcKeyGenParams) => +export const cryptoAlgorithmToAskarAlgorithm = (algorithm: KeyAlgorithm) => algorithm.name === 'ECDSA' ? KeyAlgs.EcSecp256r1 : ((algorithm.namedCurve ? algorithm.namedCurve : algorithm.name) as KeyAlgs) + +export const askarAlgorithmToCryptoAlgorithm = (algorithm: KeyAlgs): KeyAlgorithm => { + switch (algorithm) { + case KeyAlgs.EcSecp256r1: + return { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'SHA-256' } } + default: + throw new Error(`Unsupported askar algorithm: ${algorithm}`) + } +} diff --git a/src/index.ts b/src/index.ts index 3aa0774..2e2f832 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,7 @@ class Subtle extends core.SubtleCrypto { super() this.providers.set(new EcdsaProvider()) - this.providers.set(new Ed25519Provider()) - this.providers.set(new Sha1Provider()) } } diff --git a/src/storage.ts b/src/storage.ts deleted file mode 100644 index a9b580f..0000000 --- a/src/storage.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as core from 'webcrypto-core' -import { type Key as AskarKey, KeyAlgs } from '@hyperledger/aries-askar-shared' -import type { KeyType, KeyUsage } from './types' - -const keyStorage = new WeakMap() - -export function getCryptoKey(key: core.CryptoKey) { - const res = keyStorage.get(key) - if (!res) { - throw new core.OperationError('Cannot get CryptoKey from secure storage') - } - return res -} - -export function setCryptoKey({ - extractable, - askarKey, - keyType, - keyUsages, -}: { - askarKey: AskarKey - keyType: KeyType - extractable: boolean - keyUsages: KeyUsage[] -}) { - const webCryptoAlgorithm = askarAlgorithmToWebCryptoAlgorithm(askarKey.algorithm) - - const key = core.CryptoKey.create(webCryptoAlgorithm, keyType, extractable, keyUsages) - - Object.freeze(key) - - keyStorage.set(key, askarKey) - - return key -} - -const askarAlgorithmToWebCryptoAlgorithm = (alg: KeyAlgs) => { - switch (alg) { - case KeyAlgs.Ed25519: - return { name: KeyAlgs.Ed25519.toString() } - - case KeyAlgs.EcSecp256r1: - return { - name: 'ECDSA', - namedCurve: KeyAlgs.EcSecp256r1.toString(), - hash: { name: 'SHA-256' }, - } - - default: - throw new Error(`Unsupported algorithm to convert: ${alg}`) - } -} diff --git a/src/types.ts b/src/types.ts index c73561f..e260dd9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,9 @@ import type { Jwk } from '@hyperledger/aries-askar-shared' -import type * as core from 'webcrypto-core' +import type { AskarCryptoKey } from './CryptoKey' export type CryptoKeyPair = { - publicKey: core.CryptoKey - privateKey: core.CryptoKey + publicKey: AskarCryptoKey + privateKey: AskarCryptoKey } export type EcdsaParams = { @@ -11,16 +11,25 @@ export type EcdsaParams = { hash: { name: 'SHA-256' | 'SHA-384' | 'SHA-512' } } +// TODO: imporove name of `KeySignParams` +export type KeySignParams = EcdsaParams + export type EcKeyGenParams = { name: 'ECDSA' namedCurve: 'P-256' + hash?: { name: 'SHA-256' } } +export type KeyAlgorithm = EcKeyGenParams + export type EcKeyImportParams = { name: 'ECDSA' namedCurve: 'P-256' + hash?: { name: 'SHA-256' } } +export type KeyImportParams = EcKeyImportParams + export type KeyUsage = 'sign' | 'verify' export type KeyFormat = 'jwk' | 'pkcs8' | 'spki' | 'raw' export type KeyType = 'private' | 'public' | 'secret' diff --git a/tests/x509.test.ts b/tests/x509.test.ts index 7063282..9076ff5 100644 --- a/tests/x509.test.ts +++ b/tests/x509.test.ts @@ -1,5 +1,5 @@ import { before, describe, it } from 'node:test' -import assert, { strict, strictEqual } from 'node:assert' +import assert, { strictEqual } from 'node:assert' import { Crypto as WebCrypto } from '@peculiar/webcrypto'