Skip to content

Instantly share code, notes, and snippets.

@jmas
Created June 30, 2024 22:05
Show Gist options
  • Save jmas/46b8bbc6b29c02eb1a6e9bee0c8f245c to your computer and use it in GitHub Desktop.
Save jmas/46b8bbc6b29c02eb1a6e9bee0c8f245c to your computer and use it in GitHub Desktop.
import crypto from 'crypto';
import querystring from 'querystring';
import path from 'path';
export function createCanonicalRequest(
method,
pathname,
query,
headers,
payload,
doubleEscape
) {
return [
method.toUpperCase(),
createCanonicalURI(
doubleEscape
? pathname
.split(/\//g)
.map((v) => encodeURIComponent(v))
.join('/')
: pathname
),
createCanonicalQueryString(query),
createCanonicalHeaders(headers),
createSignedHeaders(headers),
createCanonicalPayload(payload),
].join('\n');
}
function createCanonicalURI(uri) {
var url = path.resolve(uri);
if (uri[uri.length - 1] == '/' && url[url.length - 1] != '/') {
url += '/';
}
return url;
}
function createCanonicalPayload(payload) {
if (payload == 'UNSIGNED-PAYLOAD') {
return payload;
}
return hash(payload || '', 'hex');
}
export function createCanonicalQueryString(params) {
if (!params) {
return '';
}
if (typeof params == 'string') {
params = querystring.parse(params);
}
return Object.keys(params)
.sort()
.map(function (key) {
var values = Array.isArray(params[key]) ? params[key] : [params[key]];
return values
.sort()
.map(function (val) {
return encodeURIComponent(key) + '=' + encodeURIComponent(val);
})
.join('&');
})
.join('&');
}
export function createCanonicalHeaders(headers) {
return Object.keys(headers)
.sort()
.map(function (name) {
var values = Array.isArray(headers[name])
? headers[name]
: [headers[name]];
return (
name.toLowerCase().trim() +
':' +
values
.map(function (v) {
return v.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
})
.join(',') +
'\n'
);
})
.join('');
}
export function createSignedHeaders(headers) {
return Object.keys(headers)
.sort()
.map(function (name) {
return name.toLowerCase().trim();
})
.join(';');
}
export function createCredentialScope(time, region, service) {
return [toDate(time), region, service, 'aws4_request'].join('/');
}
export function createStringToSign(time, region, service, request) {
return [
'AWS4-HMAC-SHA256',
toTime(time),
createCredentialScope(time, region, service),
hash(request, 'hex'),
].join('\n');
}
export function createAuthorizationHeader(
key,
scope,
signedHeaders,
signature
) {
return [
'AWS4-HMAC-SHA256 Credential=' + key + '/' + scope,
'SignedHeaders=' + signedHeaders,
'Signature=' + signature,
].join(', ');
}
export function createSignature(secret, time, region, service, stringToSign) {
var h1 = hmac('AWS4' + secret, toDate(time)); // date-key
var h2 = hmac(h1, region); // region-key
var h3 = hmac(h2, service); // service-key
var h4 = hmac(h3, 'aws4_request'); // signing-key
return hmac(h4, stringToSign, 'hex');
}
export function createPresignedS3URL(name, options) {
options = options || {};
options.method = options.method || 'GET';
options.bucket = options.bucket || process.env.AWS_S3_BUCKET;
options.signSessionToken = true;
options.doubleEscape = false;
options.endpoint = options.endpoint || 's3.amazonaws.com';
return createPresignedURL(
options.method,
options.bucket + '.' + options.endpoint,
'/' + name,
's3',
'UNSIGNED-PAYLOAD',
options
);
}
export function createPresignedURL(
method,
host,
path,
service,
payload,
options
) {
options = options || {};
options.key = options.key || process.env.AWS_ACCESS_KEY_ID;
options.secret = options.secret || process.env.AWS_SECRET_ACCESS_KEY;
options.sessionToken = options.sessionToken || process.env.AWS_SESSION_TOKEN;
options.protocol = options.protocol || 'https';
options.timestamp = options.timestamp || Date.now();
options.region = options.region || process.env.AWS_REGION || 'us-east-1';
options.expires = options.expires || 86400; // 24 hours
options.headers = options.headers || {};
options.signSessionToken = options.signSessionToken || false;
options.doubleEscape =
options.doubleEscape !== undefined ? options.doubleEscape : true;
// host is required
options.headers.Host = host;
var query = options.query ? querystring.parse(options.query) : {};
query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
query['X-Amz-Credential'] =
options.key +
'/' +
createCredentialScope(options.timestamp, options.region, service);
query['X-Amz-Date'] = toTime(options.timestamp);
query['X-Amz-Expires'] = options.expires;
query['X-Amz-SignedHeaders'] = createSignedHeaders(options.headers);
// when a session token must be "signed" into the canonical request
// (needed for some services, such as s3)
if (options.sessionToken && options.signSessionToken) {
query['X-Amz-Security-Token'] = options.sessionToken;
}
var canonicalRequest = createCanonicalRequest(
method,
path,
query,
options.headers,
payload,
options.doubleEscape
);
var stringToSign = createStringToSign(
options.timestamp,
options.region,
service,
canonicalRequest
);
var signature = createSignature(
options.secret,
options.timestamp,
options.region,
service,
stringToSign
);
query['X-Amz-Signature'] = signature;
// when a session token must NOT be "signed" into the canonical request
// (needed for some services, such as IoT)
if (options.sessionToken && !options.signSessionToken) {
query['X-Amz-Security-Token'] = options.sessionToken;
} else {
delete query['X-Amz-Security-Token'];
}
return (
options.protocol + '://' + host + path + '?' + querystring.stringify(query)
);
}
function toTime(time) {
return new Date(time).toISOString().replace(/[:\-]|\.\d{3}/g, '');
}
function toDate(time) {
return toTime(time).substring(0, 8);
}
function hmac(key, string, encoding) {
return crypto
.createHmac('sha256', key)
.update(string, 'utf8')
.digest(encoding);
}
function hash(string, encoding) {
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment