|
1 | 1 | const crypto = require('crypto'); |
| 2 | +const { Crc32 } = require('@aws-crypto/crc32'); |
| 3 | +const { Crc32c } = require('@aws-crypto/crc32c'); |
| 4 | +const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt'); |
2 | 5 | const { errors: ArsenalErrors } = require('arsenal'); |
3 | 6 | const { config } = require('../../../Config'); |
4 | 7 |
|
5 | 8 | const ChecksumError = Object.freeze({ |
6 | 9 | MD5Mismatch: 'MD5Mismatch', |
| 10 | + XAmzMismatch: 'XAmzMismatch', |
7 | 11 | MissingChecksum: 'MissingChecksum', |
| 12 | + AlgoNotSupported: 'AlgoNotSupported', |
| 13 | + MultipleChecksumTypes: 'MultipleChecksumTypes', |
| 14 | + MissingCorresponding: 'MissingCorresponding' |
8 | 15 | }); |
9 | 16 |
|
| 17 | +// TEMP |
| 18 | +// https://github.com/aws/aws-sdk-js-v3/issues/6744 |
| 19 | +// https://stackoverflow.com/questions/77663519/ |
| 20 | +// does-aws-s3-allow-specifying-multiple-checksum-values-crc32-crc32c-sha1-and-s |
| 21 | +// Expecting a single x-amz-checksum- header. Multiple checksum Types are not allowed. |
| 22 | +// XAmzContentChecksumMismatch: The provided 'x-amz-checksum' header does not match what was computed. |
| 23 | + |
| 24 | +// TODO: |
| 25 | +// x-amz-checksum-algorithm x2 different |
| 26 | +// x-amz-checksum-algorithm x2 equal |
| 27 | +// x-amz-checksum-algorithm x2 + x-amz-checksum- valid x2 |
| 28 | +// x-amz-checksum-algorithm x2 + x-amz-checksum- invalid x2 |
| 29 | +// x-amz-checksum-algorithm x2 + x-amz-checksum- mismatch x2 |
| 30 | + |
| 31 | +const algorithms = { |
| 32 | + 'crc64nvme': data => uint64ToBase64(new CrtCrc64Nvme().update(data).digest()), |
| 33 | + 'crc32': data => uint32ToBase64(new Crc32().update(data).digest() >>> 0), |
| 34 | + 'crc32c': data => uint32ToBase64(new Crc32c().update(data).digest() >>> 0), |
| 35 | + 'sha1': data => crypto.createHash('sha1').update(data).digest('base64'), |
| 36 | + 'sha256': data => crypto.createHash('sha256').update(data).digest('base64'), |
| 37 | +}; |
| 38 | + |
| 39 | +function uint32ToBase64(num) { |
| 40 | + return Buffer.alloc(4).writeUInt32BE(num, 0).toString('base64'); |
| 41 | +} |
| 42 | + |
| 43 | +function uint64ToBase64(num) { |
| 44 | + return Buffer.alloc(8).writeBigUint64BE(num, 0).toString('base64'); |
| 45 | +} |
| 46 | + |
| 47 | + |
| 48 | +function validateXAmzChecksums(headers, body) { |
| 49 | + const checksumHeaders = Object.keys(headers).filter(header => header.startsWith('x-amz-checksum-')); |
| 50 | + const xAmzCheckumCnt = checksumHeaders.length; |
| 51 | + if (xAmzCheckumCnt > 1) { |
| 52 | + return { error: ChecksumError.MultipleChecksumTypes, details: null }; |
| 53 | + } |
| 54 | + |
| 55 | + if ('x-amz-sdk-checksum-algorithm' in headers) { |
| 56 | + const algo = headers['x-amz-sdk-checksum-algorithm']; |
| 57 | + if (algo in algorithms === false) { |
| 58 | + return { error: ChecksumError.AlgoNotSupported, details: null }; |
| 59 | + } |
| 60 | + |
| 61 | + if (`x-amz-checksum-${algo}` in headers === false) { |
| 62 | + return { error: ChecksumError.MissingCorresponding, details: null }; |
| 63 | + } |
| 64 | + |
| 65 | + const expected = headers[`x-amz-checksum-${algo}`]; |
| 66 | + const calculated = algorithms[algo](body); |
| 67 | + if (expected !== calculated) { |
| 68 | + return { error: ChecksumError.XAmzMismatch, details: null }; |
| 69 | + } |
| 70 | + |
| 71 | + return null; |
| 72 | + } |
| 73 | + |
| 74 | + // No x-amz-sdk-checksum-algorithm we expect one x-amz-checksum-[crc64nvme, crc32, crc32C, sha1, sha256]. |
| 75 | + const algo = checksumHeaders[0].split('-')[3]; |
| 76 | + if (algo in algorithms === false) { |
| 77 | + return { error: ChecksumError.AlgoNotSupported, details: null };; |
| 78 | + } |
| 79 | + |
| 80 | + const expected = headers[`x-amz-checksum-${algo}`]; |
| 81 | + const calculated = algorithms[algo](body); |
| 82 | + if (expected !== calculated) { |
| 83 | + return { error: ChecksumError.XAmzMismatch, details: null }; |
| 84 | + } |
| 85 | + |
| 86 | + return null; |
| 87 | +} |
| 88 | + |
10 | 89 | /** |
11 | 90 | * validateChecksumsNoChunking - Validate the checksums of a request. |
12 | 91 | * @param {object} headers - http headers |
13 | 92 | * @param {Buffer} body - http request body |
14 | 93 | * @return {object} - error |
15 | 94 | */ |
16 | 95 | function validateChecksumsNoChunking(headers, body) { |
17 | | - if (headers && 'content-md5' in headers) { |
| 96 | + if (!headers) { |
| 97 | + return { error: ChecksumError.MissingChecksum, details: null }; |
| 98 | + } |
| 99 | + |
| 100 | + let md5Present = false; |
| 101 | + if ('content-md5' in headers) { |
18 | 102 | const md5 = crypto.createHash('md5').update(body).digest('base64'); |
19 | 103 | if (md5 !== headers['content-md5']) { |
20 | 104 | return { error: ChecksumError.MD5Mismatch, details: { calculated: md5, expected: headers['content-md5'] } }; |
21 | 105 | } |
22 | 106 |
|
23 | | - return null; |
| 107 | + md5Present = true; |
| 108 | + } |
| 109 | + |
| 110 | + const err = validateXAmzChecksums(headers, body); |
| 111 | + if (err && err.error === ChecksumError.MissingChecksum && !md5Present) { |
| 112 | + // Return MissingChecksum only if no MD5 and no x-amz-checksum-. |
| 113 | + return { error: ChecksumError.MissingChecksum, details: null }; |
24 | 114 | } |
25 | 115 |
|
26 | | - return { error: ChecksumError.MissingChecksum, details: null }; |
| 116 | + return err; |
27 | 117 | } |
28 | 118 |
|
29 | 119 | function defaultValidationFunc(request, body, log) { |
30 | 120 | const err = validateChecksumsNoChunking(request.headers, body); |
31 | 121 | if (err && err.error !== ChecksumError.MissingChecksum) { |
32 | 122 | log.debug('failed checksum validation', { method: request.apiMethod }, err); |
33 | | - return ArsenalErrors.BadDigest; |
| 123 | + return ArsenalErrors.BadDigest; // FIXME: InvalidDigest vs BadDigest |
34 | 124 | } |
35 | 125 |
|
36 | 126 | return null; |
|
0 commit comments