import IAccountDataStore from "../../common/IAccountDataStore" ; import ILogger from "../../common/ILogger" ; import { computeHMACSHA256 , getURLQueries } from "../../common/utils/utils" ; import BlobStorageContext from "../context/BlobStorageContext" ; import StorageErrorFactory from "../errors/StorageErrorFactory" ; import Context from "../generated/Context" ; import IRequest from "../generated/IRequest" ; import { HeaderConstants } from "../utils/constants" ; import IAuthenticator from "./IAuthenticator" ; export default class BlobSharedKeyAuthenticator implements IAuthenticator { public constructor ( private readonly dataStore , private readonly logger ) { } public async validate ( req , context ) { const blobContext = new BlobStorageContext ( context ) ; const account = blobContext . account ! ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() Start validation against account shared key authentication.` , blobContext . contextId ) ; const authHeaderValue = req . getHeader ( HeaderConstants . AUTHORIZATION ) ; if ( authHeaderValue === undefined ) { this . logger . info ( `BlobSharedKeyAuthenticator:validate() Request doesn't include valid authentication header. Skip shared key authentication.` , blobContext . contextId ) ; return undefined ; } const accountProperties = this . dataStore . getAccount ( account ) ; if ( accountProperties === undefined ) { this . logger . error ( `BlobSharedKeyAuthenticator:validate() Invalid storage account ${ account }.` , blobContext . contextId ) ; throw StorageErrorFactory . getInvalidOperation ( blobContext . contextId ! , "Invalid storage account." ) ; } const stringToSign = [ req . getMethod ( ) . toUpperCase ( ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_ENCODING ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_LANGUAGE ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_LENGTH ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_MD5 ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_TYPE ) , this . getHeaderValueToSign ( req , HeaderConstants . DATE ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_MODIFIED_SINCE ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_MATCH ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_NONE_MATCH ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_UNMODIFIED_SINCE ) , this . getHeaderValueToSign ( req , HeaderConstants . RANGE ) ] . join ( "\n" ) + "\n" + this . getCanonicalizedHeadersString ( req ) + this . getCanonicalizedResourceString ( req , account , blobContext . authenticationPath ) ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() [STRING TO SIGN]:${ JSON . stringify ( stringToSign ) }` , blobContext . contextId ) ; const signature1 = computeHMACSHA256 ( stringToSign , accountProperties . key1 ) ; const authValue1 = `SharedKey ${ account }:${ signature1 }` ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key1: ${ authValue1 }` , blobContext . contextId ) ; if ( authHeaderValue === authValue1 ) { this . logger . info ( `BlobSharedKeyAuthenticator:validate() Signature 1 matched.` , blobContext . contextId ) ; return true ; } if ( accountProperties . key2 ) { const signature2 = computeHMACSHA256 ( stringToSign , accountProperties . key2 ) ; const authValue2 = `SharedKey ${ account }:${ signature2 }` ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key2: ${ authValue2 }` , blobContext . contextId ) ; if ( authHeaderValue === authValue2 ) { this . logger . info ( `BlobSharedKeyAuthenticator:validate() Signature 2 matched.` , blobContext . contextId ) ; return true ; } } if ( context . context . isSecondary && blobContext . authenticationPath ?. indexOf ( account ) === 1 ) { const stringToSign_secondary = [ req . getMethod ( ) . toUpperCase ( ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_ENCODING ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_LANGUAGE ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_LENGTH ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_MD5 ) , this . getHeaderValueToSign ( req , HeaderConstants . CONTENT_TYPE ) , this . getHeaderValueToSign ( req , HeaderConstants . DATE ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_MODIFIED_SINCE ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_MATCH ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_NONE_MATCH ) , this . getHeaderValueToSign ( req , HeaderConstants . IF_UNMODIFIED_SINCE ) , this . getHeaderValueToSign ( req , HeaderConstants . RANGE ) ] . join ( "\n" ) + "\n" + this . getCanonicalizedHeadersString ( req ) + this . getCanonicalizedResourceString ( req , account , blobContext . authenticationPath ?. replace ( account , account + "-secondary" ) ) ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() [STRING TO SIGN_secondary]:${ JSON . stringify ( stringToSign_secondary ) }` , blobContext . contextId ) ; const signature1_secondary = computeHMACSHA256 ( stringToSign_secondary , accountProperties . key1 ) ; const authValue1_secondary = `SharedKey ${ account }:${ signature1_secondary }` ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key1 and stringToSign with "-secondary": ${ authValue1_secondary }` , blobContext . contextId ) ; if ( authHeaderValue === authValue1_secondary ) { this . logger . info ( `BlobSharedKeyAuthenticator:validate() Signature 1_secondary matched.` , blobContext . contextId ) ; return true ; } if ( accountProperties . key2 ) { const signature2_secondary = computeHMACSHA256 ( stringToSign_secondary , accountProperties . key2 ) ; const authValue2_secondary = `SharedKey ${ account }:${ signature2_secondary }` ; this . logger . info ( `BlobSharedKeyAuthenticator:validate() Calculated authentication header based on key2: ${ authValue2_secondary }` , blobContext . contextId ) ; if ( authHeaderValue === authValue2_secondary ) { this . logger . info ( `BlobSharedKeyAuthenticator:validate() Signature 2_secondary matched.` , blobContext . contextId ) ; return true ; } } } this . logger . info ( `BlobSharedKeyAuthenticator:validate() Validation failed.` , blobContext . contextId ) ; return false ; } see https private param { WebResource } request param { string } headerName returns { string } memberof private getHeaderValueToSign ( request , headerName ) { const value = request . getHeader ( headerName ) ; if ( ! value ) { return "" ; } if ( headerName === HeaderConstants . CONTENT_LENGTH && value === "0" ) { return "" ; } return value ; } private param { IRequest } request returns { string } memberof private getCanonicalizedHeadersString ( request ) { const headers = [ ] ; const headersObject = request . getHeaders ( ) ; for ( const name in headersObject ) { if ( headersObject . hasOwnProperty ( name ) ) { const value = headersObject [ name ] ; if ( value === undefined ) { headers . push ( { name , value : "" } ) ; } else if ( typeof value === "string" ) { headers . push ( { name , value } ) ; } else { headers . push ( { name , value : value . join ( "," ) } ) ; } } } const headersArray = headers . filter ( ( value ) => { return value . name . toLowerCase ( ) . startsWith ( HeaderConstants . PREFIX_FOR_STORAGE ) ; } ) ; headersArray . sort ( ( a , b ) : number => { return a . name . toLowerCase ( ) . localeCompare ( b . name . toLowerCase ( ) ) ; } ) ; let canonicalizedHeadersStringToSign = "" ; headersArray . forEach ( ( header ) => { canonicalizedHeadersStringToSign += `${ header . name . toLowerCase ( ) . trimRight ( ) }:${ header . value . trimLeft ( ) }\n` ; } ) ; return canonicalizedHeadersStringToSign ; } private param { IRequest } request returns { string } memberof private getCanonicalizedResourceString ( request , account , authenticationPath ? ) { let path = request . getPath ( ) || "/" ; if ( authenticationPath !== undefined ) { path = authenticationPath ; } let canonicalizedResourceString = "" ; canonicalizedResourceString += `/${ account }${ path }` ; const queries = getURLQueries ( request . getUrl ( ) ) ; const lowercaseQueries = { } ; if ( queries ) { const queryKeys = [ ] ; for ( const key in queries ) { if ( queries . hasOwnProperty ( key ) ) { const lowercaseKey = key . toLowerCase ( ) ; lowercaseQueries [ lowercaseKey ] = queries [ key ] ; queryKeys . push ( lowercaseKey ) ; } } queryKeys . sort ( ) ; for ( const key of queryKeys ) { canonicalizedResourceString += `\n${ key }:${ decodeURIComponent ( lowercaseQueries [ key ] ) }` ; } } return canonicalizedResourceString ; } }