UNPKG

@aws-amplify/core

Version:
432 lines (388 loc) • 10.7 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger as Logger } from './Logger'; import { Sha256 as jsSha256 } from '@aws-crypto/sha256-js'; import { toHex } from '@aws-sdk/util-hex-encoding'; import { parse, format } from 'url'; import { DateUtils } from './Util'; const logger = new Logger('Signer'); const DEFAULT_ALGORITHM = 'AWS4-HMAC-SHA256'; const IOT_SERVICE_NAME = 'iotdevicegateway'; const encrypt = function(key, src) { const hash = new jsSha256(key); hash.update(src); return hash.digestSync(); }; const hash = function(src) { const arg = src || ''; const hash = new jsSha256(); hash.update(arg); return toHex(hash.digestSync()); }; /** * @private * RFC 3986 compliant version of encodeURIComponent */ const escape_RFC3986 = function(component) { return component.replace(/[!'()*]/g, function(c) { return '%' + c.charCodeAt(0).toString(16).toUpperCase(); }); }; /** * @private * Create canonical query string * */ const canonical_query = function(query) { if (!query || query.length === 0) { return ''; } return query .split('&') .map(e => { const key_val = e.split('='); if (key_val.length === 1) { return e; } else { const reencoded_val = escape_RFC3986(key_val[1]); return key_val[0] + '=' + reencoded_val; } }) .sort((a, b) => { const key_a = a.split('=')[0]; const key_b = b.split('=')[0]; if (key_a === key_b) { return a < b ? -1 : 1; } else { return key_a < key_b ? -1 : 1; } }) .join('&'); }; /** * @private * Create canonical headers * <pre> CanonicalHeaders = CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN CanonicalHeadersEntry = Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n' </pre> */ const canonical_headers = function(headers) { if (!headers || Object.keys(headers).length === 0) { return ''; } return ( Object.keys(headers) .map(function(key) { return { key: key.toLowerCase(), value: headers[key] ? headers[key].trim().replace(/\s+/g, ' ') : '', }; }) .sort(function(a, b) { return a.key < b.key ? -1 : 1; }) .map(function(item) { return item.key + ':' + item.value; }) .join('\n') + '\n' ); }; /** * List of header keys included in the canonical headers. * @access private */ const signed_headers = function(headers) { return Object.keys(headers) .map(function(key) { return key.toLowerCase(); }) .sort() .join(';'); }; /** * @private * Create canonical request * Refer to * {@link http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html|Create a Canonical Request} * <pre> CanonicalRequest = HTTPRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload)) </pre> */ const canonical_request = function(request) { const url_info = parse(request.url); return [ request.method || '/', encodeURIComponent(url_info.pathname).replace(/%2F/gi, '/'), canonical_query(url_info.query), canonical_headers(request.headers), signed_headers(request.headers), hash(request.data), ].join('\n'); }; const parse_service_info = function(request) { const url_info = parse(request.url), host = url_info.host; const matched = host.match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com$/); let parsed = (matched || []).slice(1, 3); if (parsed[1] === 'es') { // Elastic Search parsed = parsed.reverse(); } return { service: request.service || parsed[0], region: request.region || parsed[1], }; }; const credential_scope = function(d_str, region, service) { return [d_str, region, service, 'aws4_request'].join('/'); }; /** * @private * Create a string to sign * Refer to * {@link http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html|Create String to Sign} * <pre> StringToSign = Algorithm + \n + RequestDateTime + \n + CredentialScope + \n + HashedCanonicalRequest </pre> */ const string_to_sign = function(algorithm, canonical_request, dt_str, scope) { return [algorithm, dt_str, scope, hash(canonical_request)].join('\n'); }; /** * @private * Create signing key * Refer to * {@link http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html|Calculate Signature} * <pre> kSecret = your secret access key kDate = HMAC("AWS4" + kSecret, Date) kRegion = HMAC(kDate, Region) kService = HMAC(kRegion, Service) kSigning = HMAC(kService, "aws4_request") </pre> */ const get_signing_key = function(secret_key, d_str, service_info) { logger.debug(service_info); const k = 'AWS4' + secret_key, k_date = encrypt(k, d_str), k_region = encrypt(k_date, service_info.region), k_service = encrypt(k_region, service_info.service), k_signing = encrypt(k_service, 'aws4_request'); return k_signing; }; const get_signature = function(signing_key, str_to_sign) { return toHex(encrypt(signing_key, str_to_sign)); }; /** * @private * Create authorization header * Refer to * {@link http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html|Add the Signing Information} */ const get_authorization_header = function( algorithm, access_key, scope, signed_headers, signature ) { return [ algorithm + ' ' + 'Credential=' + access_key + '/' + scope, 'SignedHeaders=' + signed_headers, 'Signature=' + signature, ].join(', '); }; export class Signer { /** * Sign a HTTP request, add 'Authorization' header to request param * @method sign * @memberof Signer * @static * * @param {object} request - HTTP request object <pre> request: { method: GET | POST | PUT ... url: ..., headers: { header1: ... }, data: data } </pre> * @param {object} access_info - AWS access credential info <pre> access_info: { access_key: ..., secret_key: ..., session_token: ... } </pre> * @param {object} [service_info] - AWS service type and region, optional, * if not provided then parse out from url <pre> service_info: { service: ..., region: ... } </pre> * * @returns {object} Signed HTTP request */ static sign(request, access_info, service_info = null) { request.headers = request.headers || {}; if (request.body && !request.data) { throw new Error( 'The attribute "body" was found on the request object. Please use the attribute "data" instead.' ); } // datetime string and date string const dt = DateUtils.getDateWithClockOffset(), dt_str = dt.toISOString().replace(/[:\-]|\.\d{3}/g, ''), d_str = dt_str.substr(0, 8); const url_info = parse(request.url); request.headers['host'] = url_info.host; request.headers['x-amz-date'] = dt_str; if (access_info.session_token) { request.headers['X-Amz-Security-Token'] = access_info.session_token; } // Task 1: Create a Canonical Request const request_str = canonical_request(request); logger.debug(request_str); // Task 2: Create a String to Sign const serviceInfo = service_info || parse_service_info(request), scope = credential_scope(d_str, serviceInfo.region, serviceInfo.service), str_to_sign = string_to_sign( DEFAULT_ALGORITHM, request_str, dt_str, scope ); // Task 3: Calculate the Signature const signing_key = get_signing_key( access_info.secret_key, d_str, serviceInfo ), signature = get_signature(signing_key, str_to_sign); // Task 4: Adding the Signing information to the Request const authorization_header = get_authorization_header( DEFAULT_ALGORITHM, access_info.access_key, scope, signed_headers(request.headers), signature ); request.headers['Authorization'] = authorization_header; return request; } static signUrl( urlToSign: string, accessInfo: any, serviceInfo?: any, expiration?: number ): string; static signUrl( request: any, accessInfo: any, serviceInfo?: any, expiration?: number ): string; static signUrl( urlOrRequest: string | any, accessInfo: any, serviceInfo?: any, expiration?: number ): string { const urlToSign: string = typeof urlOrRequest === 'object' ? urlOrRequest.url : urlOrRequest; const method: string = typeof urlOrRequest === 'object' ? urlOrRequest.method : 'GET'; const body: any = typeof urlOrRequest === 'object' ? urlOrRequest.body : undefined; const now = DateUtils.getDateWithClockOffset() .toISOString() .replace(/[:\-]|\.\d{3}/g, ''); const today = now.substr(0, 8); // Intentionally discarding search const { search, ...parsedUrl } = parse(urlToSign, true, true); const { host } = parsedUrl; const signedHeaders = { host }; const { region, service } = serviceInfo || parse_service_info({ url: format(parsedUrl) }); const credentialScope = credential_scope(today, region, service); // IoT service does not allow the session token in the canonical request // https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html const sessionTokenRequired = accessInfo.session_token && service !== IOT_SERVICE_NAME; const queryParams = { 'X-Amz-Algorithm': DEFAULT_ALGORITHM, 'X-Amz-Credential': [accessInfo.access_key, credentialScope].join('/'), 'X-Amz-Date': now.substr(0, 16), ...(sessionTokenRequired ? { 'X-Amz-Security-Token': `${accessInfo.session_token}` } : {}), ...(expiration ? { 'X-Amz-Expires': `${expiration}` } : {}), 'X-Amz-SignedHeaders': Object.keys(signedHeaders).join(','), }; const canonicalRequest = canonical_request({ method, url: format({ ...parsedUrl, query: { ...parsedUrl.query, ...queryParams, }, }), headers: signedHeaders, data: body, }); const stringToSign = string_to_sign( DEFAULT_ALGORITHM, canonicalRequest, now, credentialScope ); const signing_key = get_signing_key(accessInfo.secret_key, today, { region, service, }); const signature = get_signature(signing_key, stringToSign); const additionalQueryParams = { 'X-Amz-Signature': signature, ...(accessInfo.session_token && { 'X-Amz-Security-Token': accessInfo.session_token, }), }; const result = format({ protocol: parsedUrl.protocol, slashes: true, hostname: parsedUrl.hostname, port: parsedUrl.port, pathname: parsedUrl.pathname, query: { ...parsedUrl.query, ...queryParams, ...additionalQueryParams, }, }); return result; } }