Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save IhostVlad/9a72b5f005389102ed48c8a45ccfa712 to your computer and use it in GitHub Desktop.
Save IhostVlad/9a72b5f005389102ed48c8a45ccfa712 to your computer and use it in GitHub Desktop.
SignAwsV4
/* 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