var AWS = require('../core'); var v4Credentials = require('./v4_credentials'); var inherit = AWS.util.inherit; /** * @api private */ var expiresHeader = 'presigned-expires'; /** * @api private */ AWS.Signers.V4 = inherit(AWS.Signers.RequestSigner, { constructor: function V4(request, serviceName, options) { AWS.Signers.RequestSigner.call(this, request); this.serviceName = serviceName; options = options || {}; this.signatureCache = typeof options.signatureCache === 'boolean' ? options.signatureCache : true; this.operation = options.operation; this.signatureVersion = options.signatureVersion; }, algorithm: 'AWS4-HMAC-SHA256', addAuthorization: function addAuthorization(credentials, date) { var datetime = AWS.util.date.iso8601(date).replace(/[:\-]|\.\d{3}/g, ''); if (this.isPresigned()) { this.updateForPresigned(credentials, datetime); } else { this.addHeaders(credentials, datetime); } this.request.headers['Authorization'] = this.authorization(credentials, datetime); }, addHeaders: function addHeaders(credentials, datetime) { this.request.headers['X-Amz-Date'] = datetime; if (credentials.sessionToken) { this.request.headers['x-amz-security-token'] = credentials.sessionToken; } }, updateForPresigned: function updateForPresigned(credentials, datetime) { var credString = this.credentialString(datetime); var qs = { 'X-Amz-Date': datetime, 'X-Amz-Algorithm': this.algorithm, 'X-Amz-Credential': credentials.accessKeyId + '/' + credString, 'X-Amz-Expires': this.request.headers[expiresHeader], 'X-Amz-SignedHeaders': this.signedHeaders() }; if (credentials.sessionToken) { qs['X-Amz-Security-Token'] = credentials.sessionToken; } if (this.request.headers['Content-Type']) { qs['Content-Type'] = this.request.headers['Content-Type']; } if (this.request.headers['Content-MD5']) { qs['Content-MD5'] = this.request.headers['Content-MD5']; } if (this.request.headers['Cache-Control']) { qs['Cache-Control'] = this.request.headers['Cache-Control']; } // need to pull in any other X-Amz-* headers AWS.util.each.call(this, this.request.headers, function(key, value) { if (key === expiresHeader) return; if (this.isSignableHeader(key)) { var lowerKey = key.toLowerCase(); // Metadata should be normalized if (lowerKey.indexOf('x-amz-meta-') === 0) { qs[lowerKey] = value; } else if (lowerKey.indexOf('x-amz-') === 0) { qs[key] = value; } } }); var sep = this.request.path.indexOf('?') >= 0 ? '&' : '?'; this.request.path += sep + AWS.util.queryParamsToString(qs); }, authorization: function authorization(credentials, datetime) { var parts = []; var credString = this.credentialString(datetime); parts.push(this.algorithm + ' Credential=' + credentials.accessKeyId + '/' + credString); parts.push('SignedHeaders=' + this.signedHeaders()); parts.push('Signature=' + this.signature(credentials, datetime)); return parts.join(', '); }, signature: function signature(credentials, datetime) { var signingKey = v4Credentials.getSigningKey( credentials, datetime.substr(0, 8), this.request.region, this.serviceName, this.signatureCache ); return AWS.util.crypto.hmac(signingKey, this.stringToSign(datetime), 'hex'); }, stringToSign: function stringToSign(datetime) { var parts = []; parts.push('AWS4-HMAC-SHA256'); parts.push(datetime); parts.push(this.credentialString(datetime)); parts.push(this.hexEncodedHash(this.canonicalString())); return parts.join('\n'); }, canonicalString: function canonicalString() { var parts = [], pathname = this.request.pathname(); if (this.serviceName !== 's3' && this.signatureVersion !== 's3v4') pathname = AWS.util.uriEscapePath(pathname); parts.push(this.request.method); parts.push(pathname); parts.push(this.request.search()); parts.push(this.canonicalHeaders() + '\n'); parts.push(this.signedHeaders()); parts.push(this.hexEncodedBodyHash()); return parts.join('\n'); }, canonicalHeaders: function canonicalHeaders() { var headers = []; AWS.util.each.call(this, this.request.headers, function (key, item) { headers.push([key, item]); }); headers.sort(function (a, b) { return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1; }); var parts = []; AWS.util.arrayEach.call(this, headers, function (item) { var key = item[0].toLowerCase(); if (this.isSignableHeader(key)) { var value = item[1]; if (typeof value === 'undefined' || value === null || typeof value.toString !== 'function') { throw AWS.util.error(new Error('Header ' + key + ' contains invalid value'), { code: 'InvalidHeader' }); } parts.push(key + ':' + this.canonicalHeaderValues(value.toString())); } }); return parts.join('\n'); }, canonicalHeaderValues: function canonicalHeaderValues(values) { return values.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, ''); }, signedHeaders: function signedHeaders() { var keys = []; AWS.util.each.call(this, this.request.headers, function (key) { key = key.toLowerCase(); if (this.isSignableHeader(key)) keys.push(key); }); return keys.sort().join(';'); }, credentialString: function credentialString(datetime) { return v4Credentials.createScope( datetime.substr(0, 8), this.request.region, this.serviceName ); }, hexEncodedHash: function hash(string) { return AWS.util.crypto.sha256(string, 'hex'); }, hexEncodedBodyHash: function hexEncodedBodyHash() { var request = this.request; if (this.isPresigned() && this.serviceName === 's3' && !request.body) { return 'UNSIGNED-PAYLOAD'; } else if (request.headers['X-Amz-Content-Sha256']) { return request.headers['X-Amz-Content-Sha256']; } else { return this.hexEncodedHash(this.request.body || ''); } }, unsignableHeaders: [ 'authorization', 'content-type', 'content-length', 'user-agent', expiresHeader, 'expect', 'x-amzn-trace-id' ], isSignableHeader: function isSignableHeader(key) { if (key.toLowerCase().indexOf('x-amz-') === 0) return true; return this.unsignableHeaders.indexOf(key) < 0; }, isPresigned: function isPresigned() { return this.request.headers[expiresHeader] ? true : false; } }); /** * @api private */ module.exports = AWS.Signers.V4;