@@ -11,6 +11,7 @@ import type {
11
11
WalletExportImportConfig ,
12
12
Logger ,
13
13
SigningProviderRegistry ,
14
+ WalletDirectEncryptCompactJwtEcdhEsOptions ,
14
15
} from '@credo-ts/core'
15
16
import type { Session } from '@hyperledger/aries-askar-shared'
16
17
@@ -28,7 +29,15 @@ import {
28
29
KeyType ,
29
30
utils ,
30
31
} 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'
32
41
import BigNumber from 'bn.js'
33
42
34
43
import { importSecureEnvironment } from '../secureEnvironment'
@@ -459,6 +468,125 @@ export abstract class AskarBaseWallet implements Wallet {
459
468
return returnValue
460
469
}
461
470
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
+
462
590
public async generateNonce ( ) : Promise < string > {
463
591
try {
464
592
// generate an 80-bit nonce suitable for AnonCreds proofs
0 commit comments