Created
July 7, 2021 10:28
-
-
Save IhostVlad/9a72b5f005389102ed48c8a45ccfa712 to your computer and use it in GitHub Desktop.
SignAwsV4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/explicit-function-return-type */ | |
const SHA256 = require('crypto-js/sha256') | |
const encHex = require('crypto-js/enc-hex') | |
const HmacSHA256 = require('crypto-js/hmac-sha256') | |
const AWS_SHA_256 = 'AWS4-HMAC-SHA256' | |
const AWS4_REQUEST = 'aws4_request' | |
const AWS4 = 'AWS4' | |
const X_AMZ_DATE = 'x-amz-date' | |
const X_AMZ_SECURITY_TOKEN = 'x-amz-security-token' | |
const HOST = 'host' | |
const AUTHORIZATION = 'Authorization' | |
const hash = (value) => SHA256(value) | |
const hexEncode = (value) => value.toString(encHex) | |
const hmac = (secret, value) => HmacSHA256(value, secret, { asBytes: true }) | |
const hashCanonicalRequest = (request) => hexEncode(hash(request)) | |
const makeCanonical = (str, isQueryString = false) => { | |
let result = encodeURI(str) | |
.replace(/!/gi, '%21') | |
.replace(/\$/gi, '%24') | |
.replace(/&/gi, '%26') | |
.replace(/'/gi, '%27') | |
.replace(/\(/gi, '%28') | |
.replace(/\)/gi, '%29') | |
.replace(/\*/gi, '%2A') | |
.replace(/\+/gi, '%2B') | |
.replace(/,/gi, '%2C') | |
.replace(/:/gi, '%3A') | |
.replace(/\?/gi, '%3F') | |
.replace(/@/gi, '%40') | |
.replace(/\[/gi, '%5B') | |
.replace(/\]/gi, '%5D') | |
if (isQueryString) { | |
result = result.replace(/\//gi, '%2F') | |
} else { | |
result = result.replace(/=/gi, '%3D') | |
} | |
return result | |
} | |
const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop) | |
const buildCanonicalQueryString = (queryParams) => { | |
if (Object.keys(queryParams).length < 1) { | |
return '' | |
} | |
const sortedQueryParams = [] | |
for (const property in queryParams) { | |
if (hasOwnProperty(queryParams, property)) { | |
sortedQueryParams.push(property) | |
} | |
} | |
sortedQueryParams.sort() | |
let canonicalQueryString = '' | |
for (let i = 0; i < sortedQueryParams.length; i++) { | |
const param = sortedQueryParams[i] | |
const value = queryParams[param] | |
const values = Array.isArray(value) ? value : [value] | |
for (let j = 0; j < values.length; j++) { | |
canonicalQueryString += `${makeCanonical(param)}=${makeCanonical(values[j], true)}&` | |
} | |
} | |
return canonicalQueryString.substr(0, canonicalQueryString.length - 1) | |
} | |
const buildCanonicalHeaders = (headers) => { | |
let canonicalHeaders = '' | |
const sortedKeys = [] | |
for (const property in headers) { | |
if (hasOwnProperty(headers, property)) { | |
sortedKeys.push(property) | |
} | |
} | |
sortedKeys.sort() | |
for (let i = 0; i < sortedKeys.length; i++) { | |
canonicalHeaders += `${sortedKeys[i].toLowerCase()}:${headers[sortedKeys[i]]}\n` | |
} | |
return canonicalHeaders | |
} | |
const buildCanonicalSignedHeaders = (headers) => { | |
const sortedKeys = [] | |
for (const property in headers) { | |
if (hasOwnProperty(headers, property)) { | |
sortedKeys.push(property.toLowerCase()) | |
} | |
} | |
sortedKeys.sort() | |
return sortedKeys.join(';') | |
} | |
const buildStringToSign = (datetime, credentialScope, hashedCanonicalRequest) => | |
`${AWS_SHA_256}\n${datetime}\n${credentialScope}\n${hashedCanonicalRequest}` | |
const buildCredentialScope = (datetime, region, service) => | |
`${datetime.substr(0, 8)}/${region}/${service}/${AWS4_REQUEST}` | |
const calculateSigningKey = (secretKey, datetime, region, service) => | |
hmac(hmac(hmac(hmac(AWS4 + secretKey, datetime.substr(0, 8)), region), service), AWS4_REQUEST) | |
const calculateSignature = (key, stringToSign) => hexEncode(hmac(key, stringToSign)) | |
const buildCanonicalRequest = (method, path, queryParams, headers, payload) => | |
`${method}\n${makeCanonical(path)}\n${buildCanonicalQueryString( | |
queryParams | |
)}\n${buildCanonicalHeaders(headers)}\n${buildCanonicalSignedHeaders(headers)}\n${hexEncode( | |
hash(payload) | |
)}` | |
const extractHostname = (url) => { | |
let hostname | |
/* eslint-disable prefer-destructuring */ | |
if (url.indexOf('://') > -1) { | |
hostname = url.split('/')[2] | |
} else { | |
hostname = url.split('/')[0] | |
} | |
hostname = hostname.split(':')[0] | |
hostname = hostname.split('?')[0] | |
/* eslint-enable prefer-destructuring */ | |
return hostname | |
} | |
const buildAuthorizationHeader = (accessKey, credentialScope, headers, signature) => | |
`${AWS_SHA_256} Credential=${accessKey}/${credentialScope}, SignedHeaders=${buildCanonicalSignedHeaders( | |
headers | |
)}, Signature=${signature}` | |
const signAwsV4 = (config, request) => { | |
const awsSigV4Client = {} | |
if (config.accessKey === undefined || config.secretKey === undefined) { | |
return null | |
} | |
awsSigV4Client.accessKey = config.accessKey | |
awsSigV4Client.secretKey = config.secretKey | |
awsSigV4Client.sessionToken = config.sessionToken | |
awsSigV4Client.serviceName = config.serviceName || 'execute-api' | |
awsSigV4Client.region = config.region || 'us-east-1' | |
const invokeUrl = config.endpoint | |
const endpoint = /(^https?:\/\/[^/]+)/g.exec(invokeUrl)[1] | |
const pathComponent = invokeUrl.substring(endpoint.length) | |
awsSigV4Client.endpoint = endpoint | |
awsSigV4Client.pathComponent = pathComponent | |
const verb = request.method.toUpperCase() | |
const path = awsSigV4Client.pathComponent + request.path | |
const { queryParams, headers } = request | |
const body = request.body != null ? request.body.toString('utf8') : '' | |
const datetime = new Date(Date.now()) | |
.toISOString() | |
.replace(/\.\d{3}Z$/, 'Z') | |
.replace(/[:-]|\.\d{3}/g, '') | |
headers[X_AMZ_DATE] = datetime | |
headers[HOST] = extractHostname(awsSigV4Client.endpoint) | |
const canonicalRequest = buildCanonicalRequest(verb, path, queryParams, headers, body) | |
const hashedCanonicalRequest = hashCanonicalRequest(canonicalRequest) | |
const credentialScope = buildCredentialScope( | |
datetime, | |
awsSigV4Client.region, | |
awsSigV4Client.serviceName | |
) | |
const stringToSign = buildStringToSign(datetime, credentialScope, hashedCanonicalRequest) | |
const signingKey = calculateSigningKey( | |
awsSigV4Client.secretKey, | |
datetime, | |
awsSigV4Client.region, | |
awsSigV4Client.serviceName | |
) | |
const signature = calculateSignature(signingKey, stringToSign) | |
headers[AUTHORIZATION] = buildAuthorizationHeader( | |
awsSigV4Client.accessKey, | |
credentialScope, | |
headers, | |
signature | |
) | |
if (awsSigV4Client.sessionToken !== undefined && awsSigV4Client.sessionToken !== '') { | |
headers[X_AMZ_SECURITY_TOKEN] = awsSigV4Client.sessionToken | |
} | |
delete headers[HOST] | |
let url = awsSigV4Client.endpoint + path | |
const queryString = buildCanonicalQueryString(queryParams) | |
if (queryString !== '') { | |
url += `?${queryString}` | |
} | |
return { | |
headers, | |
url, | |
} | |
} | |
export default signAwsV4 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment