/* * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ import { ConsoleLogger as Logger } from './Logger'; import { AWS } from './Facet'; const logger = new Logger('Signer'), url = require('url'), crypto = AWS['util'].crypto; const DEFAULT_ALGORITHM = 'AWS4-HMAC-SHA256'; const IOT_SERVICE_NAME = 'iotdevicegateway'; const encrypt = function(key, src, encoding?) { return crypto.lib.createHmac('sha256', key).update(src, 'utf8').digest(encoding); }; const hash = function(src) { const arg = src || ''; return crypto.createHash('sha256').update(arg, 'utf8').digest('hex'); }; /** * @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 *
CanonicalHeaders =
    CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN
CanonicalHeadersEntry =
    Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n'
*/ 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} *
CanonicalRequest =
    HTTPRequestMethod + '\n' +
    CanonicalURI + '\n' +
    CanonicalQueryString + '\n' +
    CanonicalHeaders + '\n' +
    SignedHeaders + '\n' +
    HexEncode(Hash(RequestPayload))
*/ const canonical_request = function(request) { const url_info = url.parse(request.url); return [ request.method || '/', encodeURIComponent(url_info.pathname).replace(/%2F/ig, '/'), 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 = url.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} *
StringToSign =
    Algorithm + \n +
    RequestDateTime + \n +
    CredentialScope + \n +
    HashedCanonicalRequest
*/ 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} *
kSecret = your secret access key
kDate = HMAC("AWS4" + kSecret, Date)
kRegion = HMAC(kDate, Region)
kService = HMAC(kRegion, Service)
kSigning = HMAC(kService, "aws4_request")
*/ 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 encrypt(signing_key, str_to_sign, 'hex'); }; /** * @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(', '); }; /** * Sign a HTTP request, add 'Authorization' header to request param * @method sign * @memberof Signer * @static * * @param {object} request - HTTP request object
request: {
    method: GET | POST | PUT ...
    url: ...,
    headers: {
        header1: ...
    },
    data: data
}
* @param {object} access_info - AWS access credential info
access_info: {
    access_key: ...,
    secret_key: ...,
    session_token: ...
}
* @param {object} [service_info] - AWS service type and region, optional, * if not provided then parse out from url
service_info: {
    service: ...,
    region: ...
}
* * @returns {object} Signed HTTP request */ const sign = function(request, access_info, service_info = null) { request.headers = request.headers || {}; // datetime string and date string const dt = new Date(), dt_str = dt.toISOString().replace(/[:\-]|\.\d{3}/g, ''), d_str = dt_str.substr(0, 8); const url_info = url.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; }; const signUrl = function(urlToSign: String, accessInfo: any, serviceInfo?: any, expiration?: Number) { const now = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, ''); const today = now.substr(0, 8); // Intentionally discarding search const {search, ...parsedUrl} = url.parse(urlToSign, true, true); const { host } = parsedUrl; const signedHeaders = { host }; const { region, service } = serviceInfo || parse_service_info({ url: 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: 'GET', url: url.format({ ...parsedUrl, query: { ...parsedUrl.query, ...queryParams }, }), headers: signedHeaders, }); 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 = url.format({ protocol: parsedUrl.protocol, slashes: true, hostname: parsedUrl.hostname, port: parsedUrl.port, pathname: parsedUrl.pathname, query: { ...parsedUrl.query, ...queryParams, ...additionalQueryParams, } }); return result; }; /** * AWS request signer. * Refer to {@link http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html|Signature Version 4} * * @class Signer */ export default class Signer { static sign = sign; static signUrl = signUrl; }