Skip to content

Commit

Permalink
feat: credo callback crypto implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <[email protected]>
  • Loading branch information
Berend Sliedrecht committed Jun 26, 2024
1 parent e71679b commit a396d36
Show file tree
Hide file tree
Showing 13 changed files with 11,592 additions and 2,517 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"format:fix": "pnpm format --write",
"lint": "biome lint .",
"lint:fix": "pnpm lint --write --unsafe",
"check-types": "pnpm build --noEmit",
"types:check": "pnpm build --noEmit",
"test": "node --import tsx --test ./tests/*.test.ts",
"release": "release-it"
},
Expand All @@ -37,12 +37,18 @@
},
"devDependencies": {
"@biomejs/biome": "1.8.1",
"@credo-ts/askar": "0.5.3",
"@credo-ts/core": "0.5.3",
"@credo-ts/node": "0.5.3",
"@hyperledger/aries-askar-nodejs": "^0.2.1",
"@peculiar/webcrypto": "^1.5.0",
"@peculiar/x509": "^1.11.0",
"@types/node": "^20.14.2",
"release-it": "^17.3.0",
"tsx": "^4.15.2",
"typescript": "~5.4.5"
},
"peerDependencies": {
"@credo-ts/core": "*"
}
}
13,582 changes: 11,103 additions & 2,479 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/CallbackKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as core from 'webcrypto-core'
import type { KeyAlgorithm, KeyType, KeyUsage } from './types'

export class CallbackCryptoKey<T> extends core.CryptoKey {
public constructor(
public key: T,
public override algorithm: KeyAlgorithm,
public override extractable: boolean,
public override type: KeyType,
public override usages: Array<KeyUsage>
) {
super()
}
}
115 changes: 115 additions & 0 deletions src/CredoCryptoCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { type AgentContext, Buffer, type JwkJson, Key, KeyType, getJwkFromJson, getJwkFromKey } from '@credo-ts/core'
import { AsnConvert, AsnParser } from '@peculiar/asn1-schema'
import { type AlgorithmIdentifier, SubjectPublicKeyInfo } from '@peculiar/asn1-x509'
import { CallbackCryptoKey } from './CallbackKey'
import type { CryptoCallback } from './CryptoCallback'
import type {
JsonWebKey,
KeyAlgorithm,
KeyFormat,
KeyImportParams,
KeySignParams,
KeyUsage,
KeyVerifyParams,
} from './types'
import { ecdsaWithSHA256 } from '@peculiar/asn1-ecc'

export class CredoCryptoCallback implements CryptoCallback<Key> {
public constructor(private agentContext: AgentContext) {}

public async sign(key: CallbackCryptoKey<Key>, message: Uint8Array, _algorithm: KeySignParams): Promise<Uint8Array> {
const signature = await this.agentContext.wallet.sign({
key: key.key,
data: Buffer.from(message),
})

return signature
}

public async verify(
key: CallbackCryptoKey<Key>,
_algorithm: KeyVerifyParams,
message: Uint8Array,
signature: Uint8Array
): Promise<boolean> {
const isValidSignature = await this.agentContext.wallet.verify({
key: key.key,
signature: Buffer.from(signature),
data: Buffer.from(message),
})

return isValidSignature
}

public async generate(algorithm: KeyAlgorithm): Promise<Key> {
const keyType = cryptoKeyAlgorithmToCredoKeyType(algorithm)

const key = await this.agentContext.wallet.createKey({
keyType,
})

return key
}

public async importKey(
format: KeyFormat,
keyData: Uint8Array | JsonWebKey,
algorithm: KeyImportParams,
extractable: boolean,
keyUsages: Array<KeyUsage>
): Promise<CallbackCryptoKey<Key>> {
if (format === 'jwk' && keyData instanceof Uint8Array) {
throw new Error('JWK format is only allowed with a jwk as key data')
}

if (format !== 'jwk' && !(keyData instanceof Uint8Array)) {
throw new Error('non-jwk formats are only allowed with a uint8array as key data')
}

switch (format.toLowerCase()) {
case 'jwk': {
const jwk = getJwkFromJson(keyData as unknown as JwkJson)
const publicKey = Key.fromPublicKey(jwk.publicKey, jwk.keyType)
return new CallbackCryptoKey(publicKey, algorithm, extractable, 'public', keyUsages)
}
case 'spki': {
const subjectPublicKey = AsnParser.parse(keyData as Uint8Array, SubjectPublicKeyInfo)

const key = new Uint8Array(subjectPublicKey.subjectPublicKey)

const keyType = spkiAlgorithmIntoCredoKeyType(subjectPublicKey.algorithm)

return new CallbackCryptoKey(Key.fromPublicKey(key, keyType), algorithm, extractable, 'public', keyUsages)
}
default:
throw new Error(`Unsupported export format: ${format}`)
}
}

public async exportKey(format: KeyFormat, key: CallbackCryptoKey<Key>): Promise<Uint8Array | JsonWebKey> {
switch (format.toLowerCase()) {
case 'jwk': {
const jwk = getJwkFromKey(key.key)
return jwk.toJson() as unknown as JsonWebKey
}
case 'spki': {
const publicKeyInfo = new SubjectPublicKeyInfo({
algorithm: ecdsaWithSHA256,
subjectPublicKey: key.key.publicKey.buffer,
})

const derEncoded = AsnConvert.serialize(publicKeyInfo)
return new Uint8Array(derEncoded)
}

default:
throw new Error(`Unsupported export format: ${format}`)
}
}
}

// TODO: proper conversion
const cryptoKeyAlgorithmToCredoKeyType = (_algorithm: KeyAlgorithm): KeyType => KeyType.P256

// TODO: proper conversion
const spkiAlgorithmIntoCredoKeyType = (_algorithm: AlgorithmIdentifier): KeyType => KeyType.P256
31 changes: 31 additions & 0 deletions src/Crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as core from 'webcrypto-core'
import { askarGetRandomValues } from './askar'
import { EcdsaCallbackProvider } from './EcdsaCallbackProvider'
import type { CryptoCallback } from './CryptoCallback'

class Subtle<T> extends core.SubtleCrypto {
public constructor(callbacks: CryptoCallback<T>) {
super()

this.providers.set(new EcdsaCallbackProvider(callbacks))
}
}

export class Crypto<T> extends core.Crypto {
public subtle: Subtle<T>

public getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (!ArrayBuffer.isView(array)) {
throw new TypeError('Input is not an array buffer view')
}
const buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength)
askarGetRandomValues(buffer)
return array
}

public constructor(callbacks: CryptoCallback<T>) {
super()

this.subtle = new Subtle(callbacks)
}
}
21 changes: 21 additions & 0 deletions src/CryptoCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { CallbackCryptoKey } from './CallbackKey'
import type { JsonWebKey, KeyAlgorithm, KeyFormat, KeyImportParams, KeySignParams, KeyUsage } from './types'

export interface CryptoCallback<T> {
sign: (key: CallbackCryptoKey<T>, message: Uint8Array, algorithm: KeySignParams) => Promise<Uint8Array>
generate: (algorithm: KeyAlgorithm) => Promise<T>
importKey: (
format: KeyFormat,
keyData: Uint8Array | JsonWebKey,
algorithm: KeyImportParams,
extractable: boolean,
keyUsages: Array<KeyUsage>
) => Promise<CallbackCryptoKey<T>>
exportKey: (format: KeyFormat, key: CallbackCryptoKey<T>) => Promise<JsonWebKey | Uint8Array>
verify: (
key: CallbackCryptoKey<T>,
algorithm: KeySignParams,
message: Uint8Array,
signature: Uint8Array
) => Promise<boolean>
}
64 changes: 64 additions & 0 deletions src/EcdsaCallbackProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as core from 'webcrypto-core'
import { CallbackCryptoKey } from './CallbackKey'
import type { CryptoCallback } from './CryptoCallback'
import type {
CallbackCryptoKeyPair,
EcKeyGenParams,
EcKeyImportParams,
EcdsaParams,
JsonWebKey,
KeyFormat,
KeyUsage,
} from './types'

export class EcdsaCallbackProvider<T> extends core.EcdsaProvider {
public constructor(private callbacks: CryptoCallback<T>) {
super()
}

public async onSign(algorithm: EcdsaParams, key: CallbackCryptoKey<T>, data: ArrayBuffer): Promise<ArrayBuffer> {
return this.callbacks.sign(key, new Uint8Array(data), algorithm)
}

public async onVerify(
algorithm: EcdsaParams,
key: CallbackCryptoKey<T>,
signature: ArrayBuffer,
data: ArrayBuffer
): Promise<boolean> {
return this.callbacks.verify(key, algorithm, new Uint8Array(data), new Uint8Array(signature))
}

public async onGenerateKey(
algorithm: EcKeyGenParams,
extractable: boolean,
keyUsages: KeyUsage[]
): Promise<CallbackCryptoKeyPair<T>> {
const key: T = await this.callbacks.generate(algorithm)

return {
publicKey: new CallbackCryptoKey(key, algorithm, extractable, 'public', keyUsages),
privateKey: new CallbackCryptoKey(key, algorithm, extractable, 'private', keyUsages),
}
}

public async onExportKey(format: KeyFormat, key: CallbackCryptoKey<T>): Promise<JsonWebKey | ArrayBuffer> {
return this.callbacks.exportKey(format, key)
}

public async onImportKey(
format: KeyFormat,
keyData: JsonWebKey | ArrayBuffer,
algorithm: EcKeyImportParams,
extractable: boolean,
keyUsages: KeyUsage[]
): Promise<CallbackCryptoKey<T>> {
return this.callbacks.importKey(
format,
ArrayBuffer.isView(keyData) ? new Uint8Array(keyData as ArrayBuffer) : (keyData as JsonWebKey),
algorithm,
extractable,
keyUsages
)
}
}
31 changes: 2 additions & 29 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,3 @@
import * as core from 'webcrypto-core'
import { Ed25519Provider } from './Ed25519Provider'
import { EcdsaProvider } from './EcdsaProvider'
import { askarGetRandomValues } from './askar'
import { Sha1Provider } from './Sha1Provider'

class Subtle extends core.SubtleCrypto {
public constructor() {
super()

this.providers.set(new EcdsaProvider())
this.providers.set(new Ed25519Provider())
this.providers.set(new Sha1Provider())
}
}

export class Crypto extends core.Crypto {
public subtle = new Subtle()

public getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (!ArrayBuffer.isView(array)) {
throw new TypeError('Input is not an array buffer view')
}
const buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength)
askarGetRandomValues(buffer)
return array
}
}

export * from './Crypto'
export * from './types'
export * from './CredoCryptoCallback'
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type { Jwk } from '@hyperledger/aries-askar-shared'
import type { AskarCryptoKey } from './CryptoKey'
import type { CallbackCryptoKey } from './CallbackKey'

export type CryptoKeyPair = {
publicKey: AskarCryptoKey
privateKey: AskarCryptoKey
}

export type CallbackCryptoKeyPair<T> = {
publicKey: CallbackCryptoKey<T>
privateKey: CallbackCryptoKey<T>
}

export type EcdsaParams = {
name: 'ECDSA'
hash: { name: 'SHA-256' | 'SHA-384' | 'SHA-512' }
Expand All @@ -14,6 +20,8 @@ export type EcdsaParams = {
// TODO: imporove name of `KeySignParams`
export type KeySignParams = EcdsaParams

export type KeyVerifyParams = EcdsaParams

export type EcKeyGenParams = {
name: 'ECDSA'
namedCurve: 'P-256'
Expand Down
Loading

0 comments on commit a396d36

Please sign in to comment.