Skip to content

Commit fe7c5db

Browse files
authored
Merge branch 'main' into feat/concurrent-message-processing
2 parents bd10210 + 715be31 commit fe7c5db

File tree

10 files changed

+573
-5
lines changed

10 files changed

+573
-5
lines changed

.changeset/spotty-hounds-relax.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@credo-ts/askar": patch
3+
"@credo-ts/core": patch
4+
---
5+
6+
feat: add direct ecdh-es jwe encryption/decryption

packages/askar/src/utils/askarKeyTypes.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { KeyAlgs } from '@hyperledger/aries-askar-shared'
44
export enum AskarKeyTypePurpose {
55
KeyManagement = 'KeyManagement',
66
Signing = 'Signing',
7+
Encryption = 'Encryption',
78
}
89

910
const keyTypeToAskarAlg = {
@@ -29,7 +30,7 @@ const keyTypeToAskarAlg = {
2930
},
3031
[KeyType.P256]: {
3132
keyAlg: KeyAlgs.EcSecp256r1,
32-
purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing],
33+
purposes: [AskarKeyTypePurpose.KeyManagement, AskarKeyTypePurpose.Signing, AskarKeyTypePurpose.Encryption],
3334
},
3435
[KeyType.K256]: {
3536
keyAlg: KeyAlgs.EcSecp256k1,

packages/askar/src/wallet/AskarBaseWallet.ts

+129-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
WalletExportImportConfig,
1212
Logger,
1313
SigningProviderRegistry,
14+
WalletDirectEncryptCompactJwtEcdhEsOptions,
1415
} from '@credo-ts/core'
1516
import type { Session } from '@hyperledger/aries-askar-shared'
1617

@@ -28,7 +29,15 @@ import {
2829
KeyType,
2930
utils,
3031
} from '@credo-ts/core'
31-
import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared'
32+
import {
33+
CryptoBox,
34+
Store,
35+
Key as AskarKey,
36+
keyAlgFromString,
37+
EcdhEs,
38+
KeyAlgs,
39+
Jwk,
40+
} from '@hyperledger/aries-askar-shared'
3241
import BigNumber from 'bn.js'
3342

3443
import { importSecureEnvironment } from '../secureEnvironment'
@@ -459,6 +468,125 @@ export abstract class AskarBaseWallet implements Wallet {
459468
return returnValue
460469
}
461470

471+
/**
472+
* Method that enables JWE encryption using ECDH-ES and AesA256Gcm and returns it as a compact JWE.
473+
* This method is specifically added to support OpenID4VP response encryption using JARM and should later be
474+
* refactored into a more generic method that supports encryption/decryption.
475+
*
476+
* @returns compact JWE
477+
*/
478+
public async directEncryptCompactJweEcdhEs({
479+
recipientKey,
480+
encryptionAlgorithm,
481+
apu,
482+
apv,
483+
data,
484+
header,
485+
}: WalletDirectEncryptCompactJwtEcdhEsOptions) {
486+
if (encryptionAlgorithm !== 'A256GCM') {
487+
throw new WalletError(`Encryption algorithm ${encryptionAlgorithm} is not supported. Only A256GCM is supported`)
488+
}
489+
490+
// Only one supported for now
491+
const encAlg = KeyAlgs.AesA256Gcm
492+
493+
// Create ephemeral key
494+
const ephemeralKey = AskarKey.generate(keyAlgFromString(recipientKey.keyType))
495+
496+
const _header = {
497+
...header,
498+
apv,
499+
apu,
500+
enc: 'A256GCM',
501+
alg: 'ECDH-ES',
502+
epk: ephemeralKey.jwkPublic,
503+
}
504+
505+
const encodedHeader = JsonEncoder.toBuffer(_header)
506+
507+
const ecdh = new EcdhEs({
508+
algId: Uint8Array.from(Buffer.from(encAlg)),
509+
apu: apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(apu)) : Uint8Array.from([]),
510+
apv: apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(apv)) : Uint8Array.from([]),
511+
})
512+
513+
const { ciphertext, tag, nonce } = ecdh.encryptDirect({
514+
encAlg,
515+
ephemeralKey,
516+
message: Uint8Array.from(data),
517+
recipientKey: AskarKey.fromPublicBytes({
518+
algorithm: keyAlgFromString(recipientKey.keyType),
519+
publicKey: recipientKey.publicKey,
520+
}),
521+
aad: Uint8Array.from(encodedHeader),
522+
})
523+
524+
const compactJwe = `${TypedArrayEncoder.toBase64URL(encodedHeader)}..${TypedArrayEncoder.toBase64URL(
525+
nonce
526+
)}.${TypedArrayEncoder.toBase64URL(ciphertext)}.${TypedArrayEncoder.toBase64URL(tag)}`
527+
return compactJwe
528+
}
529+
530+
/**
531+
* Method that enables JWE decryption using ECDH-ES and AesA256Gcm and returns it as plaintext buffer with the header.
532+
* The apv and apu values are extracted from the heaader, and thus on a higher level it should be checked that these
533+
* values are correct.
534+
*/
535+
public async directDecryptCompactJweEcdhEs({
536+
compactJwe,
537+
recipientKey,
538+
}: {
539+
compactJwe: string
540+
recipientKey: Key
541+
}): Promise<{ data: Buffer; header: Record<string, unknown> }> {
542+
// encryption key is not used (we don't use key wrapping)
543+
const [encodedHeader /* encryptionKey */, , encodedIv, encodedCiphertext, encodedTag] = compactJwe.split('.')
544+
545+
const header = JsonEncoder.fromBase64(encodedHeader)
546+
547+
if (header.alg !== 'ECDH-ES') {
548+
throw new WalletError('Only ECDH-ES alg value is supported')
549+
}
550+
if (header.enc !== 'A256GCM') {
551+
throw new WalletError('Only A256GCM enc value is supported')
552+
}
553+
if (!header.epk || typeof header.epk !== 'object') {
554+
throw new WalletError('header epk value must contain a JWK')
555+
}
556+
557+
// NOTE: we don't support custom key storage record at the moment.
558+
let askarKey: AskarKey | null | undefined
559+
if (isKeyTypeSupportedByAskarForPurpose(recipientKey.keyType, AskarKeyTypePurpose.KeyManagement)) {
560+
askarKey = await this.withSession(
561+
async (session) => (await session.fetchKey({ name: recipientKey.publicKeyBase58 }))?.key
562+
)
563+
}
564+
if (!askarKey) {
565+
throw new WalletError('Key entry not found')
566+
}
567+
568+
// Only one supported for now
569+
const encAlg = KeyAlgs.AesA256Gcm
570+
571+
const ecdh = new EcdhEs({
572+
algId: Uint8Array.from(Buffer.from(encAlg)),
573+
apu: header.apu ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apu)) : Uint8Array.from([]),
574+
apv: header.apv ? Uint8Array.from(TypedArrayEncoder.fromBase64(header.apv)) : Uint8Array.from([]),
575+
})
576+
577+
const plaintext = ecdh.decryptDirect({
578+
nonce: TypedArrayEncoder.fromBase64(encodedIv),
579+
ciphertext: TypedArrayEncoder.fromBase64(encodedCiphertext),
580+
encAlg,
581+
ephemeralKey: Jwk.fromJson(header.epk),
582+
recipientKey: askarKey,
583+
tag: TypedArrayEncoder.fromBase64(encodedTag),
584+
aad: TypedArrayEncoder.fromBase64(encodedHeader),
585+
})
586+
587+
return { data: Buffer.from(plaintext), header }
588+
}
589+
462590
public async generateNonce(): Promise<string> {
463591
try {
464592
// generate an 80-bit nonce suitable for AnonCreds proofs

packages/askar/src/wallet/__tests__/AskarWallet.test.ts

+41
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
TypedArrayEncoder,
2020
KeyDerivationMethod,
2121
Buffer,
22+
JsonEncoder,
2223
} from '@credo-ts/core'
2324
import { Store } from '@hyperledger/aries-askar-shared'
2425

@@ -170,6 +171,46 @@ describe('AskarWallet basic operations', () => {
170171
})
171172
await expect(askarWallet.verify({ key: k256Key, data: message, signature })).resolves.toStrictEqual(true)
172173
})
174+
175+
test('Encrypt and decrypt using JWE ECDH-ES', async () => {
176+
const recipientKey = await askarWallet.createKey({
177+
keyType: KeyType.P256,
178+
})
179+
180+
const apv = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString('nonce-from-auth-request'))
181+
const apu = TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(await askarWallet.generateNonce()))
182+
183+
const compactJwe = await askarWallet.directEncryptCompactJweEcdhEs({
184+
data: JsonEncoder.toBuffer({ vp_token: ['something'] }),
185+
apu,
186+
apv,
187+
encryptionAlgorithm: 'A256GCM',
188+
header: {
189+
kid: 'some-kid',
190+
},
191+
recipientKey,
192+
})
193+
194+
const { data, header } = await askarWallet.directDecryptCompactJweEcdhEs({
195+
compactJwe,
196+
recipientKey,
197+
})
198+
199+
expect(header).toEqual({
200+
kid: 'some-kid',
201+
apv,
202+
apu,
203+
enc: 'A256GCM',
204+
alg: 'ECDH-ES',
205+
epk: {
206+
kty: 'EC',
207+
crv: 'P-256',
208+
x: expect.any(String),
209+
y: expect.any(String),
210+
},
211+
})
212+
expect(JsonEncoder.fromBuffer(data)).toEqual({ vp_token: ['something'] })
213+
})
173214
})
174215

175216
describe.skip('Currently, all KeyTypes are supported by Askar natively', () => {

packages/core/src/modules/vc/W3cCredentialsModule.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { W3cCredentialsApi } from './W3cCredentialsApi'
1212
import { W3cCredentialsModuleConfig } from './W3cCredentialsModuleConfig'
1313
import { SignatureSuiteRegistry, SignatureSuiteToken } from './data-integrity/SignatureSuiteRegistry'
1414
import { W3cJsonLdCredentialService } from './data-integrity/W3cJsonLdCredentialService'
15-
import { Ed25519Signature2018 } from './data-integrity/signature-suites'
15+
import { Ed25519Signature2018, Ed25519Signature2020 } from './data-integrity/signature-suites'
1616
import { W3cJwtCredentialService } from './jwt-vc'
1717
import { W3cCredentialRepository } from './repository/W3cCredentialRepository'
1818

@@ -48,5 +48,11 @@ export class W3cCredentialsModule implements Module {
4848
],
4949
keyTypes: [KeyType.Ed25519],
5050
})
51+
dependencyManager.registerInstance(SignatureSuiteToken, {
52+
suiteClass: Ed25519Signature2020,
53+
proofType: 'Ed25519Signature2020',
54+
verificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020],
55+
keyTypes: [KeyType.Ed25519],
56+
})
5157
}
5258
}

packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { W3cCredentialsModule } from '../W3cCredentialsModule'
55
import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig'
66
import { SignatureSuiteRegistry, SignatureSuiteToken } from '../data-integrity/SignatureSuiteRegistry'
77
import { W3cJsonLdCredentialService } from '../data-integrity/W3cJsonLdCredentialService'
8-
import { Ed25519Signature2018 } from '../data-integrity/signature-suites'
8+
import { Ed25519Signature2018, Ed25519Signature2020 } from '../data-integrity/signature-suites'
99
import { W3cJwtCredentialService } from '../jwt-vc'
1010
import { W3cCredentialRepository } from '../repository'
1111

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

30-
expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(2)
30+
expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(3)
3131
expect(dependencyManager.registerInstance).toHaveBeenCalledWith(W3cCredentialsModuleConfig, module.config)
3232

3333
expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, {
@@ -36,5 +36,11 @@ describe('W3cCredentialsModule', () => {
3636
proofType: 'Ed25519Signature2018',
3737
keyTypes: [KeyType.Ed25519],
3838
})
39+
expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, {
40+
suiteClass: Ed25519Signature2020,
41+
verificationMethodTypes: ['Ed25519VerificationKey2020'],
42+
proofType: 'Ed25519Signature2020',
43+
keyTypes: [KeyType.Ed25519],
44+
})
3945
})
4046
})

0 commit comments

Comments
 (0)