Skip to content

Commit 5a19a33

Browse files
authored
fix(middleware-ssec): add logic to handle string input as specified b… (#5676)
* fix(middleware-ssec): add logic to handle string input as specified by api model * fix(middleware-ssec): removing buffer to support browser environment * fix(middleware-ssec): adding support both for browser and nodejs * fix(middleware-ssec): using previously defined implementation for decoding base64 * fix(middleware-ssec): adding an integration test * fix(middleware-ssec): removing console logs * fix(middleware-ssec): remove unused imports --------- Co-authored-by: Ran Vaknin <[email protected]>
1 parent 26783fa commit 5a19a33

File tree

3 files changed

+97
-17
lines changed

3 files changed

+97
-17
lines changed

packages/middleware-ssec/src/index.spec.ts

+50-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { ChecksumConstructor } from "@smithy/types";
2+
import * as crypto from "crypto";
23

34
import { ssecMiddleware } from "./";
45

56
describe("ssecMiddleware", () => {
67
const next = jest.fn();
78
const decoder = jest.fn().mockResolvedValue(new Uint8Array(0));
8-
const encoder = jest.fn().mockReturnValue("base64");
9+
const base64Decoder = jest.fn();
10+
const encoder1 = jest.fn();
11+
const encoder2 = jest.fn();
912
const mockHashUpdate = jest.fn();
1013
const mockHashReset = jest.fn();
1114
const mockHashDigest = jest.fn().mockReturnValue(new Uint8Array(0));
@@ -17,13 +20,54 @@ describe("ssecMiddleware", () => {
1720
beforeEach(() => {
1821
next.mockClear();
1922
decoder.mockClear();
20-
encoder.mockClear();
23+
encoder1.mockClear();
24+
encoder2.mockClear();
2125
mockHashUpdate.mockClear();
2226
mockHashDigest.mockClear();
2327
mockHashReset.mockClear();
2428
});
2529

2630
it("should base64 encode input keys and set respective MD5 inputs", async () => {
31+
encoder1.mockReturnValue("/+JF8FMG8UVMWSaNz0s6Wg==");
32+
const key = "TestKey123";
33+
const binaryRepresentationOfKey = Buffer.from(key);
34+
const base64Key = binaryRepresentationOfKey.toString("base64");
35+
const md5Hash = crypto.createHash("md5").update(binaryRepresentationOfKey).digest();
36+
const base64Md5Hash = Buffer.from(md5Hash).toString("base64");
37+
38+
const args = {
39+
input: {
40+
SSECustomerKey: base64Key,
41+
CopySourceSSECustomerKey: base64Key,
42+
},
43+
};
44+
45+
const handler = ssecMiddleware({
46+
base64Encoder: encoder1,
47+
utf8Decoder: decoder,
48+
md5: MockHash,
49+
base64Decoder: base64Decoder,
50+
})(next, {} as any);
51+
52+
await handler(args);
53+
54+
expect(next.mock.calls.length).toBe(1);
55+
expect(next).toHaveBeenCalledWith({
56+
input: {
57+
SSECustomerKey: base64Key,
58+
SSECustomerKeyMD5: base64Md5Hash,
59+
CopySourceSSECustomerKey: base64Key,
60+
CopySourceSSECustomerKeyMD5: base64Md5Hash,
61+
},
62+
});
63+
expect(decoder.mock.calls.length).toBe(0);
64+
expect(encoder1.mock.calls.length).toBe(2);
65+
expect(mockHashUpdate.mock.calls.length).toBe(2);
66+
expect(mockHashDigest.mock.calls.length).toBe(2);
67+
encoder1.mockClear();
68+
});
69+
it("should base64 encode input keys and set respective MD5 inputs", async () => {
70+
encoder2.mockReturnValue("base64");
2771
const args = {
2872
input: {
2973
SSECustomerKey: "foo",
@@ -32,9 +76,10 @@ describe("ssecMiddleware", () => {
3276
};
3377

3478
const handler = ssecMiddleware({
35-
base64Encoder: encoder,
79+
base64Encoder: encoder2,
3680
utf8Decoder: decoder,
3781
md5: MockHash,
82+
base64Decoder: base64Decoder,
3883
})(next, {} as any);
3984

4085
await handler(args);
@@ -49,8 +94,9 @@ describe("ssecMiddleware", () => {
4994
},
5095
});
5196
expect(decoder.mock.calls.length).toBe(2);
52-
expect(encoder.mock.calls.length).toBe(4);
97+
expect(encoder2.mock.calls.length).toBe(4);
5398
expect(mockHashUpdate.mock.calls.length).toBe(2);
5499
expect(mockHashDigest.mock.calls.length).toBe(2);
100+
encoder2.mockClear();
55101
});
56102
});

packages/middleware-ssec/src/index.ts

+20-13
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ interface PreviouslyResolved {
1616
base64Encoder: Encoder;
1717
md5: ChecksumConstructor | HashConstructor;
1818
utf8Decoder: Decoder;
19+
base64Decoder: Decoder;
1920
}
2021

2122
export function ssecMiddleware(options: PreviouslyResolved): InitializeMiddleware<any, any> {
2223
return <Output extends MetadataBearer>(next: InitializeHandler<any, Output>): InitializeHandler<any, Output> =>
2324
async (args: InitializeHandlerArguments<any>): Promise<InitializeHandlerOutput<Output>> => {
24-
let input = { ...args.input };
25+
const input = { ...args.input };
2526
const properties = [
2627
{
2728
target: "SSECustomerKey",
@@ -36,19 +37,25 @@ export function ssecMiddleware(options: PreviouslyResolved): InitializeMiddlewar
3637
for (const prop of properties) {
3738
const value: SourceData | undefined = (input as any)[prop.target];
3839
if (value) {
39-
const valueView: Uint8Array = ArrayBuffer.isView(value)
40-
? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
41-
: typeof value === "string"
42-
? options.utf8Decoder(value)
43-
: new Uint8Array(value);
44-
const encoded = options.base64Encoder(valueView);
40+
let valueForHash: Uint8Array;
41+
if (typeof value === "string") {
42+
const isBase64Encoded = /^(?:[A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
43+
if (isBase64Encoded) {
44+
valueForHash = options.base64Decoder(value);
45+
} else {
46+
valueForHash = options.utf8Decoder(value);
47+
input[prop.target] = options.base64Encoder(valueForHash);
48+
}
49+
} else {
50+
valueForHash = ArrayBuffer.isView(value)
51+
? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
52+
: new Uint8Array(value);
53+
input[prop.target] = options.base64Encoder(valueForHash);
54+
}
55+
4556
const hash = new options.md5();
46-
hash.update(valueView);
47-
input = {
48-
...(input as any),
49-
[prop.target]: encoded,
50-
[prop.hash]: options.base64Encoder(await hash.digest()),
51-
};
57+
hash.update(valueForHash);
58+
input[prop.hash] = options.base64Encoder(await hash.digest());
5259
}
5360
}
5461

packages/middleware-ssec/src/middleware-ssec.integ.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { S3 } from "@aws-sdk/client-s3";
2+
import * as crypto from "crypto";
23

34
import { requireRequestsFrom } from "../../../private/aws-util-test/src";
45

@@ -29,5 +30,31 @@ describe("middleware-ssec", () => {
2930
Key: "k",
3031
});
3132
});
33+
34+
it("verifies headers for PutObject with base64-encoded SSECustomerKey", async () => {
35+
const client = new S3({ region: "us-east-1" });
36+
requireRequestsFrom(client).toMatch({
37+
method: "PUT",
38+
hostname: "testbucket.s3.us-east-1.amazonaws.com",
39+
query: { "x-id": "PutObject" },
40+
headers: {
41+
"x-amz-server-side-encryption-customer-algorithm": "AES256",
42+
"x-amz-server-side-encryption-customer-key": "UNhY4JhezH9gQYqvDMWrWH9CwlcKiECVqejMrND2VFw=",
43+
"x-amz-server-side-encryption-customer-key-md5": "SwoBWUcJBbc/WRhR6hZGCA==",
44+
"content-length": "14",
45+
},
46+
body: "This is a test",
47+
protocol: "https:",
48+
path: "/foo",
49+
});
50+
const exampleKey = crypto.createHash("sha256").update("example").digest();
51+
await client.putObject({
52+
Bucket: "testbucket",
53+
Body: "This is a test",
54+
Key: "foo",
55+
SSECustomerKey: exampleKey.toString("base64"),
56+
SSECustomerAlgorithm: "AES256",
57+
});
58+
});
3259
});
3360
});

0 commit comments

Comments
 (0)