-
Notifications
You must be signed in to change notification settings - Fork 2k
/
createSignedURL.js
147 lines (133 loc) · 5.85 KB
/
createSignedURL.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* 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 {object} param0
* @param {string} param0.method – The HTTP method.
* @param {string} 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 {string} 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 {Record<string, string>} 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 {string} 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.
* @returns {string}
*/
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
* @param {Record<string,string>} param0
* @returns {Promise<URL>} the signed URL
*/
export default async function createSignedURL ({
accountKey, accountSecret, sessionToken,
bucketName,
Key, Region,
expires,
uploadId, partNumber,
}) {
const Service = 's3'
const host = `${bucketName}.${Service}.${Region}.amazonaws.com`
const CanonicalUri = `/${encodeURI(Key)}`
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
}