Skip to content

Commit

Permalink
Merge branch 'main' into feat/concurrent-message-processing
Browse files Browse the repository at this point in the history
  • Loading branch information
sairanjit authored Sep 29, 2024
2 parents bd10210 + 715be31 commit fe7c5db
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/spotty-hounds-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@credo-ts/askar": patch
"@credo-ts/core": patch
---

feat: add direct ecdh-es jwe encryption/decryption
3 changes: 2 additions & 1 deletion packages/askar/src/utils/askarKeyTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { KeyAlgs } from '@hyperledger/aries-askar-shared'
export enum AskarKeyTypePurpose {
KeyManagement = 'KeyManagement',
Signing = 'Signing',
Encryption = 'Encryption',
}

const keyTypeToAskarAlg = {
Expand All @@ -29,7 +30,7 @@ const keyTypeToAskarAlg = {
},
[KeyType.P256]: {
keyAlg: KeyAlgs.EcSecp256r1,
purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing],
purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing, AskarKeyTypePurpose.Encryption],
},
[KeyType.K256]: {
keyAlg: KeyAlgs.EcSecp256k1,
Expand Down
130 changes: 129 additions & 1 deletion packages/askar/src/wallet/AskarBaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
WalletExportImportConfig,
Logger,
SigningProviderRegistry,
WalletDirectEncryptCompactJwtEcdhEsOptions,
} from '@credo-ts/core'
import type { Session } from '@hyperledger/aries-askar-shared'

Expand All @@ -28,7 +29,15 @@ import {
KeyType,
utils,
} from '@credo-ts/core'
import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared'
import {
CryptoBox,
Store,
Key as AskarKey,
keyAlgFromString,
EcdhEs,
KeyAlgs,
Jwk,
} from '@hyperledger/aries-askar-shared'
import BigNumber from 'bn.js'

import { importSecureEnvironment } from '../secureEnvironment'
Expand Down Expand Up @@ -459,6 +468,125 @@ export abstract class AskarBaseWallet implements Wallet {
return returnValue
}

/**
* Method that enables JWE encryption using ECDH-ES and AesA256Gcm and returns it as a compact JWE.
* This method is specifically added to support OpenID4VP response encryption using JARM and should later be
* refactored into a more generic method that supports encryption/decryption.
*
* @returns compact JWE
*/
public async directEncryptCompactJweEcdhEs({
recipientKey,
encryptionAlgorithm,
apu,
apv,
data,
header,
}: WalletDirectEncryptCompactJwtEcdhEsOptions) {
if (encryptionAlgorithm !== 'A256GCM') {
throw new WalletError(`Encryption algorithm ${encryptionAlgorithm} is not supported. Only A256GCM is supported`)
}

// Only one supported for now
const encAlg = KeyAlgs.AesA256Gcm

// Create ephemeral key
const ephemeralKey = AskarKey.generate(keyAlgFromString(recipientKey.keyType))

const _header = {
...header,
apv,
apu,
enc: 'A256GCM',
alg: 'ECDH-ES',
epk: ephemeralKey.jwkPublic,
}

const encodedHeader = JsonEncoder.toBuffer(_header)

const ecdh = new EcdhEs({
algId: Uint8Array.from(Buffer.from(encAlg)),
apu: apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(apu)) : Uint8Array.from([]),
apv: apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(apv)) : Uint8Array.from([]),
})

const { ciphertext, tag, nonce } = ecdh.encryptDirect({
encAlg,
ephemeralKey,
message: Uint8Array.from(data),
recipientKey: AskarKey.fromPublicBytes({
algorithm: keyAlgFromString(recipientKey.keyType),
publicKey: recipientKey.publicKey,
}),
aad: Uint8Array.from(encodedHeader),
})

const compactJwe = `${TypedArrayEncoder.toBase64URL(encodedHeader)}..${TypedArrayEncoder.toBase64URL(
nonce
)}.${TypedArrayEncoder.toBase64URL(ciphertext)}.${TypedArrayEncoder.toBase64URL(tag)}`
return compactJwe
}

/**
* Method that enables JWE decryption using ECDH-ES and AesA256Gcm and returns it as plaintext buffer with the header.
* The apv and apu values are extracted from the heaader, and thus on a higher level it should be checked that these
* values are correct.
*/
public async directDecryptCompactJweEcdhEs({
compactJwe,
recipientKey,
}: {
compactJwe: string
recipientKey: Key
}): Promise<{ data: Buffer; header: Record<string, unknown> }> {
// encryption key is not used (we don't use key wrapping)
const [encodedHeader /* encryptionKey */, , encodedIv, encodedCiphertext, encodedTag] = compactJwe.split('.')

const header = JsonEncoder.fromBase64(encodedHeader)

if (header.alg !== 'ECDH-ES') {
throw new WalletError('Only ECDH-ES alg value is supported')
}
if (header.enc !== 'A256GCM') {
throw new WalletError('Only A256GCM enc value is supported')
}
if (!header.epk || typeof header.epk !== 'object') {
throw new WalletError('header epk value must contain a JWK')
}

// NOTE: we don't support custom key storage record at the moment.
let askarKey: AskarKey | null | undefined
if (isKeyTypeSupportedByAskarForPurpose(recipientKey.keyType, AskarKeyTypePurpose.KeyManagement)) {
askarKey = await this.withSession(
async (session) => (await session.fetchKey({ name: recipientKey.publicKeyBase58 }))?.key
)
}
if (!askarKey) {
throw new WalletError('Key entry not found')
}

// Only one supported for now
const encAlg = KeyAlgs.AesA256Gcm

const ecdh = new EcdhEs({
algId: Uint8Array.from(Buffer.from(encAlg)),
apu: header.apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apu)) : Uint8Array.from([]),
apv: header.apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apv)) : Uint8Array.from([]),
})

const plaintext = ecdh.decryptDirect({
nonce: TypedArrayEncoder.fromBase64(encodedIv),
ciphertext: TypedArrayEncoder.fromBase64(encodedCiphertext),
encAlg,
ephemeralKey: Jwk.fromJson(header.epk),
recipientKey: askarKey,
tag: TypedArrayEncoder.fromBase64(encodedTag),
aad: TypedArrayEncoder.fromBase64(encodedHeader),
})

return { data: Buffer.from(plaintext), header }
}

public async generateNonce(): Promise<string> {
try {
// generate an 80-bit nonce suitable for AnonCreds proofs
Expand Down
41 changes: 41 additions & 0 deletions packages/askar/src/wallet/__tests__/AskarWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
TypedArrayEncoder,
KeyDerivationMethod,
Buffer,
JsonEncoder,
} from '@credo-ts/core'
import { Store } from '@hyperledger/aries-askar-shared'

Expand Down Expand Up @@ -170,6 +171,46 @@ describe('AskarWallet basic operations', () => {
})
await expect(askarWallet.verify({ key: k256Key, data: message, signature })).resolves.toStrictEqual(true)
})

test('Encrypt and decrypt using JWE ECDH-ES', async () => {
const recipientKey = await askarWallet.createKey({
keyType: KeyType.P256,
})

const apv = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString('nonce-from-auth-request'))
const apu = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(await askarWallet.generateNonce()))

const compactJwe = await askarWallet.directEncryptCompactJweEcdhEs({
data: JsonEncoder.toBuffer({ vp_token: ['something'] }),
apu,
apv,
encryptionAlgorithm: 'A256GCM',
header: {
kid: 'some-kid',
},
recipientKey,
})

const { data, header } = await askarWallet.directDecryptCompactJweEcdhEs({
compactJwe,
recipientKey,
})

expect(header).toEqual({
kid: 'some-kid',
apv,
apu,
enc: 'A256GCM',
alg: 'ECDH-ES',
epk: {
kty: 'EC',
crv: 'P-256',
x: expect.any(String),
y: expect.any(String),
},
})
expect(JsonEncoder.fromBuffer(data)).toEqual({ vp_token: ['something'] })
})
})

describe.skip('Currently, all KeyTypes are supported by Askar natively', () => {
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/modules/vc/W3cCredentialsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { W3cCredentialsApi } from './W3cCredentialsApi'
import { W3cCredentialsModuleConfig } from './W3cCredentialsModuleConfig'
import { SignatureSuiteRegistry, SignatureSuiteToken } from './data-integrity/SignatureSuiteRegistry'
import { W3cJsonLdCredentialService } from './data-integrity/W3cJsonLdCredentialService'
import { Ed25519Signature2018 } from './data-integrity/signature-suites'
import { Ed25519Signature2018, Ed25519Signature2020 } from './data-integrity/signature-suites'
import { W3cJwtCredentialService } from './jwt-vc'
import { W3cCredentialRepository } from './repository/W3cCredentialRepository'

Expand Down Expand Up @@ -48,5 +48,11 @@ export class W3cCredentialsModule implements Module {
],
keyTypes: [KeyType.Ed25519],
})
dependencyManager.registerInstance(SignatureSuiteToken, {
suiteClass: Ed25519Signature2020,
proofType: 'Ed25519Signature2020',
verificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020],
keyTypes: [KeyType.Ed25519],
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { W3cCredentialsModule } from '../W3cCredentialsModule'
import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig'
import { SignatureSuiteRegistry, SignatureSuiteToken } from '../data-integrity/SignatureSuiteRegistry'
import { W3cJsonLdCredentialService } from '../data-integrity/W3cJsonLdCredentialService'
import { Ed25519Signature2018 } from '../data-integrity/signature-suites'
import { Ed25519Signature2018, Ed25519Signature2020 } from '../data-integrity/signature-suites'
import { W3cJwtCredentialService } from '../jwt-vc'
import { W3cCredentialRepository } from '../repository'

Expand All @@ -27,7 +27,7 @@ describe('W3cCredentialsModule', () => {
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(W3cCredentialRepository)
expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SignatureSuiteRegistry)

expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(2)
expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(3)
expect(dependencyManager.registerInstance).toHaveBeenCalledWith(W3cCredentialsModuleConfig, module.config)

expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, {
Expand All @@ -36,5 +36,11 @@ describe('W3cCredentialsModule', () => {
proofType: 'Ed25519Signature2018',
keyTypes: [KeyType.Ed25519],
})
expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, {
suiteClass: Ed25519Signature2020,
verificationMethodTypes: ['Ed25519VerificationKey2020'],
proofType: 'Ed25519Signature2020',
keyTypes: [KeyType.Ed25519],
})
})
})
Loading

0 comments on commit fe7c5db

Please sign in to comment.