UNPKG

@uppy/aws-s3

Version:

Upload to Amazon S3 with Uppy

129 lines (128 loc) 6.35 kB
/** * Create a canonical request by concatenating the following strings, separated * by newline characters. This helps ensure that the signature that you * calculate and the signature that AWS calculates can match. * * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request * * @param param0 * @param param0.method – The HTTP method. * @param param0.CanonicalUri – The URI-encoded version of the absolute * path component URL (everything between the host and the question mark * character (?) that starts the query string parameters). If the absolute path * is empty, use a forward slash character (/). * @param param0.CanonicalQueryString – The URL-encoded query string * parameters, separated by ampersands (&). Percent-encode reserved characters, * including the space character. Encode names and values separately. If there * are empty parameters, append the equals sign to the parameter name before * encoding. After encoding, sort the parameters alphabetically by key name. If * there is no query string, use an empty string (""). * @param param0.SignedHeaders – The request headers, * that will be signed, and their values, separated by newline characters. * For the values, trim any leading or trailing spaces, convert sequential * spaces to a single space, and separate the values for a multi-value header * using commas. You must include the host header (HTTP/1.1), and any x-amz-* * headers in the signature. You can optionally include other standard headers * in the signature, such as content-type. * @param param0.HashedPayload – A string created using the payload in * the body of the HTTP request as input to a hash function. This string uses * lowercase hexadecimal characters. If the payload is empty, use an empty * string as the input to the hash function. */ function createCanonicalRequest({ method = 'PUT', CanonicalUri = '/', CanonicalQueryString = '', SignedHeaders, HashedPayload, }) { const headerKeys = Object.keys(SignedHeaders) .map((k) => k.toLowerCase()) .sort(); return [ method, CanonicalUri, CanonicalQueryString, ...headerKeys.map((k) => `${k}:${SignedHeaders[k]}`), '', headerKeys.join(';'), HashedPayload, ].join('\n'); } const ec = new TextEncoder(); const algorithm = { name: 'HMAC', hash: 'SHA-256' }; async function digest(data) { const { subtle } = globalThis.crypto; return subtle.digest(algorithm.hash, ec.encode(data)); } async function generateHmacKey(secret) { const { subtle } = globalThis.crypto; return subtle.importKey('raw', typeof secret === 'string' ? ec.encode(secret) : secret, algorithm, false, ['sign']); } function arrayBufferToHexString(arrayBuffer) { const byteArray = new Uint8Array(arrayBuffer); let hexString = ''; for (let i = 0; i < byteArray.length; i++) { hexString += byteArray[i].toString(16).padStart(2, '0'); } return hexString; } async function hash(key, data) { const { subtle } = globalThis.crypto; return subtle.sign(algorithm, await generateHmacKey(key), ec.encode(data)); } /** * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html */ export default async function createSignedURL({ accountKey, accountSecret, sessionToken, bucketName, Key, Region, expires, uploadId, partNumber, }) { const Service = 's3'; const host = `${Service}.${Region}.amazonaws.com`; /** * List of char out of `encodeURI()` is taken from ECMAScript spec. * Note that the `/` character is purposefully not included in list below. * * @see https://tc39.es/ecma262/#sec-encodeuri-uri */ const CanonicalUri = `/${bucketName}/${encodeURI(Key).replace(/[;?:@&=+$,#!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`; const payload = 'UNSIGNED-PAYLOAD'; const requestDateTime = new Date().toISOString().replace(/[-:]|\.\d+/g, ''); // YYYYMMDDTHHMMSSZ const date = requestDateTime.slice(0, 8); // YYYYMMDD const scope = `${date}/${Region}/${Service}/aws4_request`; const url = new URL(`https://${host}${CanonicalUri}`); // N.B.: URL search params needs to be added in the ASCII order url.searchParams.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256'); url.searchParams.set('X-Amz-Content-Sha256', payload); url.searchParams.set('X-Amz-Credential', `${accountKey}/${scope}`); url.searchParams.set('X-Amz-Date', requestDateTime); url.searchParams.set('X-Amz-Expires', expires); // We are signing on the client, so we expect there's going to be a session token: url.searchParams.set('X-Amz-Security-Token', sessionToken); url.searchParams.set('X-Amz-SignedHeaders', 'host'); // Those two are present only for Multipart Uploads: if (partNumber) url.searchParams.set('partNumber', partNumber); if (uploadId) url.searchParams.set('uploadId', uploadId); url.searchParams.set('x-id', partNumber && uploadId ? 'UploadPart' : 'PutObject'); // Step 1: Create a canonical request const canonical = createCanonicalRequest({ CanonicalUri, CanonicalQueryString: url.search.slice(1), SignedHeaders: { host, }, HashedPayload: payload, }); // Step 2: Create a hash of the canonical request const hashedCanonical = arrayBufferToHexString(await digest(canonical)); // Step 3: Create a string to sign const stringToSign = [ `AWS4-HMAC-SHA256`, // The algorithm used to create the hash of the canonical request. requestDateTime, // The date and time used in the credential scope. scope, // The credential scope. This restricts the resulting signature to the specified Region and service. hashedCanonical, // The hash of the canonical request. ].join('\n'); // Step 4: Calculate the signature const kDate = await hash(`AWS4${accountSecret}`, date); const kRegion = await hash(kDate, Region); const kService = await hash(kRegion, Service); const kSigning = await hash(kService, 'aws4_request'); const signature = arrayBufferToHexString(await hash(kSigning, stringToSign)); // Step 5: Add the signature to the request url.searchParams.set('X-Amz-Signature', signature); return url; }