import { needs , NodeEncryptionMaterial , NodeDecryptionMaterial , unwrapDataKey , wrapWithKeyObjectIfSupported , AwsEsdkKeyObject , NodeHash , } from '@aws-crypto/material-management' ; import { createCipheriv , createDecipheriv , createSign , createVerify , timingSafeEqual , } from 'crypto' ; import { HKDF } from '@aws-crypto/hkdf-node' ; import { kdfInfo , kdfCommitKeyInfo } from '@aws-crypto/serialize' ; import { AwsESDKSigner , AwsESDKVerify } from './types' ; export interface AwsEsdkJsCipherGCM { update ( data ) : Buffer ; final ( ) : Buffer ; getAuthTag ( ) : Buffer ; setAAD ( aad ) : this ; } export interface AwsEsdkJsDecipherGCM { update ( data ) : Buffer ; final ( ) : Buffer ; setAuthTag ( buffer ) : this ; setAAD ( aad ) : this ; } type KDFIndex = Readonly < { [ K in NodeHash ] : ReturnType < typeof HKDF > ; } > ; const kdfIndex = Object . freeze ( { sha256 : HKDF ( 'sha256' as NodeHash ) , sha384 : HKDF ( 'sha384' as NodeHash ) , sha512 : HKDF ( 'sha512' as NodeHash ) , } ) ; export interface GetCipher { ( iv ) : AwsEsdkJsCipherGCM ; } export interface GetCipherInfo { ( messageId ) : { getCipher : GetCipher ; keyCommitment ? : Uint8Array ; } ; } export interface GetSigner { ( ) : AwsESDKSigner & { awsCryptoSign : ( ) => Buffer ; } ; } export interface NodeEncryptionMaterialHelper { getCipherInfo : GetCipherInfo ; getSigner ? : GetSigner ; dispose : ( ) => void ; } export interface GetEncryptHelper { ( material ) : NodeEncryptionMaterialHelper ; } export const getEncryptHelper = ( material ) => { needs ( material . hasValidKey ( ) , 'Material has no unencrypted data key.' ) ; const { signatureHash } = material . suite ; const getCipherInfo = curryCryptoStream ( material , createCipheriv ) ; return Object . freeze ( { getCipherInfo , getSigner : signatureHash ? getSigner : undefined , dispose , } ) ; function getSigner ( ) { needs ( material . hasUnencryptedDataKey , 'Unencrypted data key has been zeroed.' ) ; if ( ! signatureHash ) throw new Error ( 'Material does not support signature.' ) ; const { signatureKey } = material ; if ( ! signatureKey ) throw new Error ( 'Material does not support signature.' ) ; const { privateKey } = signatureKey ; if ( typeof privateKey !== 'string' ) throw new Error ( 'Material does not support signature.' ) ; const signer = Object . assign ( createSign ( signatureHash ) , { awsCryptoSign : ( ) => signer . sign ( privateKey ) } ) ; return signer ; } function dispose ( ) { material . zeroUnencryptedDataKey ( ) ; } } ; export interface GetDecipher { ( iv ) : AwsEsdkJsDecipherGCM ; } export interface GetDecipherInfo { ( messageId , commitKey ? ) : GetDecipher ; } export interface GetVerify { ( ) : AwsESDKVerify & { awsCryptoVerify : ( signature : Buffer ) => boolean ; } ; } export interface NodeDecryptionMaterialHelper { getDecipherInfo : GetDecipherInfo ; getVerify ? : GetVerify ; dispose : ( ) => void ; } export interface GetDecryptionHelper { ( material ) : NodeDecryptionMaterialHelper ; } export const getDecryptionHelper = ( material ) => { needs ( material . hasValidKey ( ) , 'Material has no unencrypted data key.' ) ; const { signatureHash } = material . suite ; const getDecipherInfo = curryCryptoStream ( material , createDecipheriv ) ; return Object . freeze ( { getDecipherInfo , getVerify : signatureHash ? getVerify : undefined , dispose , } ) ; function getVerify ( ) { if ( ! signatureHash ) throw new Error ( 'Material does not support signature.' ) ; const { verificationKey } = material ; if ( ! verificationKey ) throw new Error ( 'Material does not support signature.' ) ; const verify = Object . assign ( createVerify ( signatureHash ) , { awsCryptoVerify : ( signature ) => verify . verify ( verificationKey . publicKey as string , signature ) , } ) ; return verify ; } function dispose ( ) { material . zeroUnencryptedDataKey ( ) ; } } ; type CreateCryptoIvStream < Material extends NodeEncryptionMaterial | NodeDecryptionMaterial > = Material extends NodeEncryptionMaterial ? typeof createCipheriv : typeof createDecipheriv ; type CryptoStream < Material extends NodeEncryptionMaterial | NodeDecryptionMaterial > = Material extends NodeEncryptionMaterial ? AwsEsdkJsCipherGCM : AwsEsdkJsDecipherGCM ; type CreateCryptoStream < Material extends NodeEncryptionMaterial | NodeDecryptionMaterial > = ( iv : Uint8Array ) => CryptoStream < Material > ; type CurryHelper < Material extends NodeEncryptionMaterial | NodeDecryptionMaterial > = Material extends NodeEncryptionMaterial ? { getCipher : CreateCryptoStream < Material > ; keyCommitment : Uint8Array ; } : Material extends NodeDecryptionMaterial ? CreateCryptoStream < Material > : never ; export function curryCryptoStream < Material extends NodeEncryptionMaterial | NodeDecryptionMaterial > ( material , createCryptoIvStream ) { const { encryption : cipherName , ivLength } = material . suite ; const isEncrypt = material instanceof NodeEncryptionMaterial ; needs ( isEncrypt ? createCipheriv === createCryptoIvStream : material instanceof NodeDecryptionMaterial ? createDecipheriv === createCryptoIvStream : false , 'Unsupported cryptographic material.' ) ; return ( messageId , commitKey ? ) => { const { derivedKey , keyCommitment } = nodeKdf ( material , messageId , commitKey ) ; return ( isEncrypt ? { getCipher : createCryptoStream , keyCommitment } : createCryptoStream ) as CurryHelper < Material > ; function createCryptoStream ( iv ) { needs ( iv . byteLength === ivLength , 'Iv length does not match algorithm suite specification' ) ; needs ( material . hasUnencryptedDataKey , 'Unencrypted data key has been zeroed.' ) ; return createCryptoIvStream ( cipherName , derivedKey as any , iv ) as unknown as CryptoStream < Material > ; } } ; } export function nodeKdf ( material , nonce , commitKey ? ) { const dataKey = material . getUnencryptedDataKey ( ) ; const { kdf , kdfHash , keyLengthBytes , commitmentLength , saltLengthBytes , commitment , id : suiteId , } = material . suite ; if ( ! kdf && ! kdfHash ) { needs ( ! commitKey , 'Commitment not supported.' ) ; return { derivedKey : dataKey } ; } needs ( kdf === 'HKDF' && kdfHash && kdfIndex [ kdfHash ] && nonce instanceof Uint8Array , 'Invalid HKDF values.' ) ; const { buffer : dkBuffer , byteOffset : dkByteOffset , byteLength : dkByteLength , } = unwrapDataKey ( dataKey ) ; if ( commitment === 'NONE' ) { needs ( ! commitKey , 'Commitment not supported.' ) ; const toExtract = Buffer . from ( dkBuffer , dkByteOffset , dkByteLength ) ; const { buffer , byteOffset , byteLength } = kdfInfo ( suiteId , nonce ) ; const infoBuff = Buffer . from ( buffer , byteOffset , byteLength ) ; const derivedBytes = kdfIndex [ kdfHash as NodeHash ] ( toExtract ) ( keyLengthBytes , infoBuff ) ; const derivedKey = wrapWithKeyObjectIfSupported ( derivedBytes ) ; return { derivedKey } ; } needs ( commitment === 'KEY' && commitmentLength && saltLengthBytes , 'Malformed suite data.' ) ; needs ( nonce . byteLength === saltLengthBytes , 'Nonce is not the correct length for committed algorithm suite.' ) ; const toExtract = Buffer . from ( dkBuffer , dkByteOffset , dkByteLength ) ; const expand = kdfIndex [ kdfHash as NodeHash ] ( toExtract , nonce ) ; const { keyLabel , commitLabel } = kdfCommitKeyInfo ( material . suite ) ; const keyCommitment = expand ( commitmentLength / 8 , commitLabel ) ; const isDecrypt = material instanceof NodeDecryptionMaterial ; needs ( ( isDecrypt && commitKey && timingSafeEqual ( keyCommitment , commitKey ) ) || ( ! isDecrypt && ! commitKey ) , isDecrypt ? 'Commitment does not match.' : 'Invalid arguments.' ) ; const derivedBytes = expand ( keyLengthBytes , keyLabel ) ; const derivedKey = wrapWithKeyObjectIfSupported ( derivedBytes ) ; return { derivedKey , keyCommitment } ; }