import { Transform as PortableTransform } from 'readable-stream' ; import { Transform } from 'stream' ; import { needs , GetVerify , GetDecipher , } from '@aws-crypto/material-management-node' ; import { deserializeSignature , decodeBodyHeader , BodyHeader , HeaderInfo , serializeMessageHeaderAuth , } from '@aws-crypto/serialize' ; import { ParseHeaderStream } from './parse_header_stream' ; import { DecipherInfo } from './decipher_stream' ; type AWSVerify = ReturnType < GetVerify > ; const PortableTransformWithType = PortableTransform as new ( ... args ) => Transform ; export interface VerifyInfo { headerInfo : HeaderInfo ; getDecipher : GetDecipher ; dispose : ( ) => void ; verify ? : AWSVerify ; } export interface VerifyStreamOptions { maxBodySize ? : number ; } interface VerifyState { buffer : Buffer ; authTagBuffer : Buffer ; currentFrame ? : BodyHeader ; signatureInfo : Buffer ; sequenceNumber : number ; finalAuthTagReceived : boolean ; } export class VerifyStream extends PortableTransformWithType { private _headerInfo ! : HeaderInfo ; private _verifyState : VerifyState = { buffer : Buffer . alloc ( 0 ) , authTagBuffer : Buffer . alloc ( 0 ) , signatureInfo : Buffer . alloc ( 0 ) , sequenceNumber : 0 , finalAuthTagReceived : false , } ; private _verify ? : AWSVerify ; private _maxBodySize ? : number ; constructor ( { maxBodySize } ) { super ( ) ; needs ( ! maxBodySize || typeof maxBodySize === 'number' , 'Unsupported MaxBodySize.' ) ; Object . defineProperty ( this , '_maxBodySize' , { value : maxBodySize , enumerable : true , } ) ; this . on ( 'pipe' , ( source ) => { needs ( source instanceof ParseHeaderStream , 'Unsupported source' ) ; source . once ( 'VerifyInfo' , ( verifyInfo ) => { const { getDecipher , verify , headerInfo , dispose } = verifyInfo ; const { messageId , contentType } = headerInfo . messageHeader ; if ( verify ) { const { rawHeader , headerAuth , messageHeader } = headerInfo ; const { headerIv , headerAuthTag } = headerAuth ; verify . update ( < Buffer > rawHeader ) ; verify . update ( < Buffer > serializeMessageHeaderAuth ( { headerIv , headerAuthTag , messageHeader , } ) ) ; } Object . defineProperty ( this , '_headerInfo' , { value : headerInfo , enumerable : true , } ) ; Object . defineProperty ( this , '_verify' , { value : verify , enumerable : true , } ) ; const decipherInfo = { messageId : Buffer . from ( ( messageId as Uint8Array ) . buffer || messageId , ( messageId as Uint8Array ) . byteOffset || 0 , messageId . byteLength ) , contentType , getDecipher , dispose , } ; this . emit ( 'DecipherInfo' , decipherInfo ) ; } ) ; } ) ; } _transform ( chunk , enc , callback ) { needs ( this . _headerInfo , 'VerifyStream not configured, VerifyInfo event not yet received.' ) ; const state = this . _verifyState ; const { currentFrame } = state ; if ( ! currentFrame ) { const { buffer } = state ; const frameBuffer = Buffer . concat ( [ buffer , chunk ] ) ; const frameHeader = decodeBodyHeader ( frameBuffer , this . _headerInfo , 0 ) ; if ( ! frameHeader ) { state . buffer = frameBuffer ; return callback ( ) ; } needs ( ! this . _maxBodySize || this . _maxBodySize >= frameHeader . contentLength , 'maxBodySize exceeded.' ) ; state . sequenceNumber += 1 ; needs ( frameHeader . sequenceNumber === state . sequenceNumber , 'Encrypted body sequence out of order.' ) ; if ( this . _verify ) { this . _verify . update ( frameBuffer . slice ( 0 , frameHeader . readPos ) ) ; } const tail = frameBuffer . slice ( frameHeader . readPos ) ; this . emit ( 'BodyInfo' , frameHeader ) ; state . currentFrame = frameHeader ; return setImmediate ( ( ) => this . _transform ( tail , enc , callback ) ) ; } const { contentLength } = currentFrame ; if ( chunk . length && contentLength > 0 ) { if ( contentLength > chunk . length ) { currentFrame . contentLength -= chunk . length ; this . push ( chunk ) ; return callback ( ) ; } else { const content = chunk . slice ( 0 , contentLength ) ; const tail = chunk . slice ( content . length ) ; this . push ( content ) ; currentFrame . contentLength = 0 ; return setImmediate ( ( ) => this . _transform ( tail , enc , callback ) ) ; } } const { tagLength } = currentFrame ; const tagLengthBytes = tagLength / 8 ; const { authTagBuffer } = state ; if ( chunk . length && tagLengthBytes > authTagBuffer . length ) { const left = tagLengthBytes - authTagBuffer . length ; if ( left > chunk . length ) { state . authTagBuffer = Buffer . concat ( [ authTagBuffer , chunk ] ) ; return callback ( ) ; } else { const finalAuthTagBuffer = Buffer . concat ( [ authTagBuffer , chunk ] , tagLengthBytes ) ; if ( this . _verify ) { this . _verify . update ( finalAuthTagBuffer ) ; } state . buffer = Buffer . alloc ( 0 ) ; state . currentFrame = undefined ; state . authTagBuffer = Buffer . alloc ( 0 ) ; if ( currentFrame . isFinalFrame ) { this . _verifyState . finalAuthTagReceived = true ; this . _transform = ( chunk , _enc , callback ) => { if ( chunk . length ) { state . signatureInfo = Buffer . concat ( [ state . signatureInfo , chunk ] ) ; } callback ( ) ; } ; } const tail = chunk . slice ( left ) ; const next = ( ) => this . _transform ( tail , enc , callback ) ; return this . emit ( 'AuthTag' , finalAuthTagBuffer , next ) ; } } callback ( ) ; } push ( chunk , encoding ? ) { if ( this . _verify && chunk ) { this . _verify . update ( chunk ) ; } return super . push ( chunk , encoding ) ; } _flush ( callback ) { const { finalAuthTagReceived } = this . _verifyState ; if ( ! finalAuthTagReceived ) return callback ( new Error ( 'Incomplete message' ) ) ; if ( ! this . _verify ) return callback ( ) ; try { const { signatureInfo } = this . _verifyState ; const { buffer , byteOffset , byteLength } = deserializeSignature ( signatureInfo ) ; const signature = Buffer . from ( buffer , byteOffset , byteLength ) ; const isVerified = this . _verify . awsCryptoVerify ( signature ) ; needs ( isVerified , 'Invalid Signature' ) ; callback ( ) ; } catch ( e ) { callback ( e ) ; } } }