File: //var/www/api-storage/node_modules/minio/src/signing.ts
/*
* MinIO Javascript Library for Amazon S3 Compatible Cloud Storage, (C) 2016 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 * as crypto from 'node:crypto'
import * as errors from './errors.ts'
import { PRESIGN_EXPIRY_DAYS_MAX } from './helpers.ts'
import { getScope, isNumber, isObject, isString, makeDateLong, makeDateShort, uriEscape } from './internal/helper.ts'
import type { ICanonicalRequest, IRequest, RequestHeaders } from './internal/type.ts'
const signV4Algorithm = 'AWS4-HMAC-SHA256'
// getCanonicalRequest generate a canonical request of style.
//
// canonicalRequest =
// <HTTPMethod>\n
// <CanonicalURI>\n
// <CanonicalQueryString>\n
// <CanonicalHeaders>\n
// <SignedHeaders>\n
// <HashedPayload>
//
function getCanonicalRequest(
method: string,
path: string,
headers: RequestHeaders,
signedHeaders: string[],
hashedPayload: string,
): ICanonicalRequest {
if (!isString(method)) {
throw new TypeError('method should be of type "string"')
}
if (!isString(path)) {
throw new TypeError('path should be of type "string"')
}
if (!isObject(headers)) {
throw new TypeError('headers should be of type "object"')
}
if (!Array.isArray(signedHeaders)) {
throw new TypeError('signedHeaders should be of type "array"')
}
if (!isString(hashedPayload)) {
throw new TypeError('hashedPayload should be of type "string"')
}
const headersArray = signedHeaders.reduce((acc, i) => {
// Trim spaces from the value (required by V4 spec)
const val = `${headers[i]}`.replace(/ +/g, ' ')
acc.push(`${i.toLowerCase()}:${val}`)
return acc
}, [] as string[])
const requestResource = path.split('?')[0]
let requestQuery = path.split('?')[1]
if (!requestQuery) {
requestQuery = ''
}
if (requestQuery) {
requestQuery = requestQuery
.split('&')
.sort()
.map((element) => (!element.includes('=') ? element + '=' : element))
.join('&')
}
return [
method.toUpperCase(),
requestResource,
requestQuery,
headersArray.join('\n') + '\n',
signedHeaders.join(';').toLowerCase(),
hashedPayload,
].join('\n')
}
// generate a credential string
function getCredential(accessKey: string, region: string, requestDate?: Date, serviceName = 's3') {
if (!isString(accessKey)) {
throw new TypeError('accessKey should be of type "string"')
}
if (!isString(region)) {
throw new TypeError('region should be of type "string"')
}
if (!isObject(requestDate)) {
throw new TypeError('requestDate should be of type "object"')
}
return `${accessKey}/${getScope(region, requestDate, serviceName)}`
}
// Returns signed headers array - alphabetically sorted
function getSignedHeaders(headers: RequestHeaders): string[] {
if (!isObject(headers)) {
throw new TypeError('request should be of type "object"')
}
// Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258
//
// User-Agent:
//
// This is ignored from signing because signing this causes problems with generating pre-signed URLs
// (that are executed by other agents) or when customers pass requests through proxies, which may
// modify the user-agent.
//
// Content-Length:
//
// This is ignored from signing because generating a pre-signed URL should not provide a content-length
// constraint, specifically when vending a S3 pre-signed PUT URL. The corollary to this is that when
// sending regular requests (non-pre-signed), the signature contains a checksum of the body, which
// implicitly validates the payload length (since changing the number of bytes would change the checksum)
// and therefore this header is not valuable in the signature.
//
// Content-Type:
//
// Signing this header causes quite a number of problems in browser environments, where browsers
// like to modify and normalize the content-type header in different ways. There is more information
// on this in https://github.com/aws/aws-sdk-js/issues/244. Avoiding this field simplifies logic
// and reduces the possibility of future bugs
//
// Authorization:
//
// Is skipped for obvious reasons
const ignoredHeaders = ['authorization', 'content-length', 'content-type', 'user-agent']
return Object.keys(headers)
.filter((header) => !ignoredHeaders.includes(header))
.sort()
}
// returns the key used for calculating signature
function getSigningKey(date: Date, region: string, secretKey: string, serviceName = 's3') {
if (!isObject(date)) {
throw new TypeError('date should be of type "object"')
}
if (!isString(region)) {
throw new TypeError('region should be of type "string"')
}
if (!isString(secretKey)) {
throw new TypeError('secretKey should be of type "string"')
}
const dateLine = makeDateShort(date)
const hmac1 = crypto
.createHmac('sha256', 'AWS4' + secretKey)
.update(dateLine)
.digest(),
hmac2 = crypto.createHmac('sha256', hmac1).update(region).digest(),
hmac3 = crypto.createHmac('sha256', hmac2).update(serviceName).digest()
return crypto.createHmac('sha256', hmac3).update('aws4_request').digest()
}
// returns the string that needs to be signed
function getStringToSign(canonicalRequest: ICanonicalRequest, requestDate: Date, region: string, serviceName = 's3') {
if (!isString(canonicalRequest)) {
throw new TypeError('canonicalRequest should be of type "string"')
}
if (!isObject(requestDate)) {
throw new TypeError('requestDate should be of type "object"')
}
if (!isString(region)) {
throw new TypeError('region should be of type "string"')
}
const hash = crypto.createHash('sha256').update(canonicalRequest).digest('hex')
const scope = getScope(region, requestDate, serviceName)
const stringToSign = [signV4Algorithm, makeDateLong(requestDate), scope, hash]
return stringToSign.join('\n')
}
// calculate the signature of the POST policy
export function postPresignSignatureV4(region: string, date: Date, secretKey: string, policyBase64: string): string {
if (!isString(region)) {
throw new TypeError('region should be of type "string"')
}
if (!isObject(date)) {
throw new TypeError('date should be of type "object"')
}
if (!isString(secretKey)) {
throw new TypeError('secretKey should be of type "string"')
}
if (!isString(policyBase64)) {
throw new TypeError('policyBase64 should be of type "string"')
}
const signingKey = getSigningKey(date, region, secretKey)
return crypto.createHmac('sha256', signingKey).update(policyBase64).digest('hex').toLowerCase()
}
// Returns the authorization header
export function signV4(
request: IRequest,
accessKey: string,
secretKey: string,
region: string,
requestDate: Date,
sha256sum: string,
serviceName = 's3',
) {
if (!isObject(request)) {
throw new TypeError('request should be of type "object"')
}
if (!isString(accessKey)) {
throw new TypeError('accessKey should be of type "string"')
}
if (!isString(secretKey)) {
throw new TypeError('secretKey should be of type "string"')
}
if (!isString(region)) {
throw new TypeError('region should be of type "string"')
}
if (!accessKey) {
throw new errors.AccessKeyRequiredError('accessKey is required for signing')
}
if (!secretKey) {
throw new errors.SecretKeyRequiredError('secretKey is required for signing')
}
const signedHeaders = getSignedHeaders(request.headers)
const canonicalRequest = getCanonicalRequest(request.method, request.path, request.headers, signedHeaders, sha256sum)
const serviceIdentifier = serviceName || 's3'
const stringToSign = getStringToSign(canonicalRequest, requestDate, region, serviceIdentifier)
const signingKey = getSigningKey(requestDate, region, secretKey, serviceIdentifier)
const credential = getCredential(accessKey, region, requestDate, serviceIdentifier)
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex').toLowerCase()
return `${signV4Algorithm} Credential=${credential}, SignedHeaders=${signedHeaders
.join(';')
.toLowerCase()}, Signature=${signature}`
}
export function signV4ByServiceName(
request: IRequest,
accessKey: string,
secretKey: string,
region: string,
requestDate: Date,
contentSha256: string,
serviceName = 's3',
): string {
return signV4(request, accessKey, secretKey, region, requestDate, contentSha256, serviceName)
}
// returns a presigned URL string
export function presignSignatureV4(
request: IRequest,
accessKey: string,
secretKey: string,
sessionToken: string | undefined,
region: string,
requestDate: Date,
expires: number | undefined,
) {
if (!isObject(request)) {
throw new TypeError('request should be of type "object"')
}
if (!isString(accessKey)) {
throw new TypeError('accessKey should be of type "string"')
}
if (!isString(secretKey)) {
throw new TypeError('secretKey should be of type "string"')
}
if (!isString(region)) {
throw new TypeError('region should be of type "string"')
}
if (!accessKey) {
throw new errors.AccessKeyRequiredError('accessKey is required for presigning')
}
if (!secretKey) {
throw new errors.SecretKeyRequiredError('secretKey is required for presigning')
}
if (expires && !isNumber(expires)) {
throw new TypeError('expires should be of type "number"')
}
if (expires && expires < 1) {
throw new errors.ExpiresParamError('expires param cannot be less than 1 seconds')
}
if (expires && expires > PRESIGN_EXPIRY_DAYS_MAX) {
throw new errors.ExpiresParamError('expires param cannot be greater than 7 days')
}
const iso8601Date = makeDateLong(requestDate)
const signedHeaders = getSignedHeaders(request.headers)
const credential = getCredential(accessKey, region, requestDate)
const hashedPayload = 'UNSIGNED-PAYLOAD'
const requestQuery: string[] = []
requestQuery.push(`X-Amz-Algorithm=${signV4Algorithm}`)
requestQuery.push(`X-Amz-Credential=${uriEscape(credential)}`)
requestQuery.push(`X-Amz-Date=${iso8601Date}`)
requestQuery.push(`X-Amz-Expires=${expires}`)
requestQuery.push(`X-Amz-SignedHeaders=${uriEscape(signedHeaders.join(';').toLowerCase())}`)
if (sessionToken) {
requestQuery.push(`X-Amz-Security-Token=${uriEscape(sessionToken)}`)
}
const resource = request.path.split('?')[0]
let query = request.path.split('?')[1]
if (query) {
query = query + '&' + requestQuery.join('&')
} else {
query = requestQuery.join('&')
}
const path = resource + '?' + query
const canonicalRequest = getCanonicalRequest(request.method, path, request.headers, signedHeaders, hashedPayload)
const stringToSign = getStringToSign(canonicalRequest, requestDate, region)
const signingKey = getSigningKey(requestDate, region, secretKey)
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex').toLowerCase()
return request.protocol + '//' + request.headers.host + path + `&X-Amz-Signature=${signature}`
}