-
Notifications
You must be signed in to change notification settings - Fork 19
feat: put object with instruction file configured #466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 22 commits
8e51328
898c55e
fd64f1c
95b3b5a
3546538
ed761d9
a34b347
d6da2cf
60feafd
288ee65
9c1c50d
ae6b6b4
c9378cf
ef353d7
fbdbe92
9e25d8f
7bf259c
ff5e99c
a396a7c
b1c0898
b4b2047
b9ae158
4f51c42
bf2be6e
02f6f48
c319d44
0ed41a5
0349dec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,93 @@ | ||
| // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| package software.amazon.encryption.s3.internal; | ||
|
|
||
| import software.amazon.awssdk.protocols.jsoncore.JsonWriter; | ||
| import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; | ||
| import software.amazon.awssdk.services.s3.model.PutObjectRequest; | ||
| import software.amazon.encryption.s3.S3EncryptionClientException; | ||
| import software.amazon.encryption.s3.materials.EncryptedDataKey; | ||
| import software.amazon.encryption.s3.materials.EncryptionMaterials; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Base64; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| @FunctionalInterface | ||
| public interface ContentMetadataEncodingStrategy { | ||
| public class ContentMetadataEncodingStrategy { | ||
|
|
||
| Map<String, String> encodeMetadata(EncryptionMaterials materials, byte[] iv, | ||
| Map<String, String> metadata); | ||
| private static final Base64.Encoder ENCODER = Base64.getEncoder(); | ||
| private final InstructionFileConfig _instructionFileConfig; | ||
|
|
||
| public ContentMetadataEncodingStrategy(InstructionFileConfig instructionFileConfig) { | ||
| _instructionFileConfig = instructionFileConfig; | ||
| } | ||
|
|
||
| public PutObjectRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, PutObjectRequest putObjectRequest) { | ||
| if (_instructionFileConfig.isInstructionFilePutEnabled()) { | ||
| final String metadataString = metadataToString(materials, iv); | ||
| _instructionFileConfig.putInstructionFile(putObjectRequest, metadataString); | ||
| // the original request object is returned as-is | ||
| return putObjectRequest; | ||
| } else { | ||
| Map<String, String> newMetadata = addMetadataToMap(putObjectRequest.metadata(), materials, iv); | ||
| return putObjectRequest.toBuilder() | ||
| .metadata(newMetadata) | ||
| .build(); | ||
| } | ||
| } | ||
|
|
||
| public CreateMultipartUploadRequest encodeMetadata(EncryptionMaterials materials, byte[] iv, CreateMultipartUploadRequest createMultipartUploadRequest) { | ||
| if(_instructionFileConfig.isInstructionFilePutEnabled()) { | ||
| final String metadataString = metadataToString(materials, iv); | ||
| PutObjectRequest putObjectRequest = ConvertSDKRequests.convertRequest(createMultipartUploadRequest); | ||
| _instructionFileConfig.putInstructionFile(putObjectRequest, metadataString); | ||
| // the original request object is returned as-is | ||
| return createMultipartUploadRequest; | ||
| } else { | ||
| Map<String, String> newMetadata = addMetadataToMap(createMultipartUploadRequest.metadata(), materials, iv); | ||
| return createMultipartUploadRequest.toBuilder() | ||
| .metadata(newMetadata) | ||
| .build(); | ||
| } | ||
| } | ||
| private String metadataToString(EncryptionMaterials materials, byte[] iv) { | ||
| // this is just the metadata map serialized as JSON | ||
| // so first get the Map | ||
| final Map<String, String> metadataMap = addMetadataToMap(new HashMap<>(), materials, iv); | ||
| // then serialize it | ||
| try (JsonWriter jsonWriter = JsonWriter.create()) { | ||
| jsonWriter.writeStartObject(); | ||
| for (Map.Entry<String, String> entry : metadataMap.entrySet()) { | ||
| jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); | ||
| } | ||
| jsonWriter.writeEndObject(); | ||
|
|
||
| return new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); | ||
| } catch (JsonWriter.JsonGenerationException e) { | ||
| throw new S3EncryptionClientException("Cannot serialize materials to JSON.", e); | ||
| } | ||
| } | ||
|
|
||
| private Map<String, String> addMetadataToMap(Map<String, String> map, EncryptionMaterials materials, byte[] iv) { | ||
| Map<String, String> metadata = new HashMap<>(map); | ||
| EncryptedDataKey edk = materials.encryptedDataKeys().get(0); | ||
| metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_V2, ENCODER.encodeToString(edk.encryptedDatakey())); | ||
| metadata.put(MetadataKeyConstants.CONTENT_IV, ENCODER.encodeToString(iv)); | ||
| metadata.put(MetadataKeyConstants.CONTENT_CIPHER, materials.algorithmSuite().cipherName()); | ||
| metadata.put(MetadataKeyConstants.CONTENT_CIPHER_TAG_LENGTH, Integer.toString(materials.algorithmSuite().cipherTagLengthBits())); | ||
| metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_ALGORITHM, new String(edk.keyProviderInfo(), StandardCharsets.UTF_8)); | ||
|
|
||
| try (JsonWriter jsonWriter = JsonWriter.create()) { | ||
| jsonWriter.writeStartObject(); | ||
| for (Map.Entry<String, String> entry : materials.encryptionContext().entrySet()) { | ||
| jsonWriter.writeFieldName(entry.getKey()).writeValue(entry.getValue()); | ||
| } | ||
| jsonWriter.writeEndObject(); | ||
|
|
||
| String jsonEncryptionContext = new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); | ||
| metadata.put(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT, jsonEncryptionContext); | ||
| } catch (JsonWriter.JsonGenerationException e) { | ||
| throw new S3EncryptionClientException("Cannot serialize encryption context to JSON.", e); | ||
| } | ||
| return metadata; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,131 @@ | |||||||||||||
|
|
||||||||||||||
| public class ConvertSDKRequests { | ||||||||||||||
|
|
||||||||||||||
| public static PutObjectRequest convertRequest(CreateMultipartUploadRequest request) { | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: We should add some comments here, i.e. that this is used to set the optional fields for Instruction File puts when the request is a multipart upload, because the instruction file is Put with an ordinary PutObject request. And likewise for the other |
||||||||||||||
|
|
||||||||||||||
| final PutObjectRequest.Builder output = PutObjectRequest.builder(); | ||||||||||||||
| request | ||||||||||||||
| .toBuilder() | ||||||||||||||
| .sdkFields() | ||||||||||||||
| .forEach(f -> { | ||||||||||||||
| final Object value = f.getValueOrDefault(request); | ||||||||||||||
| if (value != null) { | ||||||||||||||
| switch (f.memberName()) { | ||||||||||||||
| case "ACL": | ||||||||||||||
| output.acl((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "Bucket": | ||||||||||||||
| output.bucket((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "BucketKeyEnabled": | ||||||||||||||
| output.bucketKeyEnabled((Boolean) value); | ||||||||||||||
| break; | ||||||||||||||
| case "CacheControl": | ||||||||||||||
| output.cacheControl((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ChecksumAlgorithm": | ||||||||||||||
| output.checksumAlgorithm((String) value); | ||||||||||||||
| break; | ||||||||||||||
|
lucasmcdonald3 marked this conversation as resolved.
|
||||||||||||||
| case "ContentDisposition": | ||||||||||||||
| assert value instanceof String; | ||||||||||||||
| output.contentDisposition((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ContentEncoding": | ||||||||||||||
| output.contentEncoding((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ContentLanguage": | ||||||||||||||
| output.contentLanguage((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ContentType": | ||||||||||||||
| output.contentType((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ExpectedBucketOwner": | ||||||||||||||
| output.expectedBucketOwner((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "Expires": | ||||||||||||||
| output.expires((Instant) value); | ||||||||||||||
| break; | ||||||||||||||
| case "GrantFullControl": | ||||||||||||||
| output.grantFullControl((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "GrantRead": | ||||||||||||||
| output.grantRead((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "GrantReadACP": | ||||||||||||||
| output.grantReadACP((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "GrantWriteACP": | ||||||||||||||
| output.grantWriteACP((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "Key": | ||||||||||||||
| output.key((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "Metadata": | ||||||||||||||
| if (!isStringStringMap(value)) { | ||||||||||||||
| throw new IllegalArgumentException("Metadata must be a Map<String, String>"); | ||||||||||||||
| } | ||||||||||||||
| @SuppressWarnings("unchecked") | ||||||||||||||
| Map<String, String> metadata = (Map<String, String>) value; | ||||||||||||||
| output.metadata(metadata); | ||||||||||||||
| break; | ||||||||||||||
| case "ObjectLockLegalHoldStatus": | ||||||||||||||
| output.objectLockLegalHoldStatus((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ObjectLockMode": | ||||||||||||||
| output.objectLockMode((String) value); | ||||||||||||||
| break; | ||||||||||||||
|
lucasmcdonald3 marked this conversation as resolved.
|
||||||||||||||
| case "ObjectLockRetainUntilDate": | ||||||||||||||
| output.objectLockRetainUntilDate((Instant) value); | ||||||||||||||
| break; | ||||||||||||||
| case "RequestPayer": | ||||||||||||||
| output.requestPayer((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "ServerSideEncryption": | ||||||||||||||
| output.serverSideEncryption((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "SSECustomerAlgorithm": | ||||||||||||||
| output.sseCustomerAlgorithm((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "SSECustomerKey": | ||||||||||||||
| output.sseCustomerKey((String) value); | ||||||||||||||
| break; | ||||||||||||||
|
lucasmcdonald3 marked this conversation as resolved.
|
||||||||||||||
| case "SSEKMSEncryptionContext": | ||||||||||||||
| output.ssekmsEncryptionContext((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "SSEKMSKeyId": | ||||||||||||||
| output.ssekmsKeyId((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "StorageClass": | ||||||||||||||
| output.storageClass((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "Tagging": | ||||||||||||||
| output.tagging((String) value); | ||||||||||||||
| break; | ||||||||||||||
| case "WebsiteRedirectLocation": | ||||||||||||||
| output.websiteRedirectLocation((String) value); | ||||||||||||||
| break; | ||||||||||||||
| default: | ||||||||||||||
| // Rather than silently dropping the value, | ||||||||||||||
| // we loudly signal that we don't know how to handle this field. | ||||||||||||||
| throw new IllegalArgumentException( | ||||||||||||||
| f.locationName() + " is an unknown field. " + | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Please fix this in the other |
||||||||||||||
| "The S3 Encryption Client does not recognize this option and cannot set it on the PutObjectRequest." + | ||||||||||||||
| "This may be a new S3 feature." + | ||||||||||||||
| "Please report this to the Amazon S3 Encryption Client for Java: " + | ||||||||||||||
| "https://github.com/aws/amazon-s3-encryption-client-java/issues." + | ||||||||||||||
| "To work around this issue you can disable multi part upload," + | ||||||||||||||
| "use the Async client, or not set this value on PutObject." + | ||||||||||||||
| "You may be able to update this value after the PutObject request completes." | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The text should be slightly different as it only applies to cases where InstructionFile puts are enabled. |
||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
| return output | ||||||||||||||
| // OverrideConfiguration is not as SDKField but still needs to be supported | ||||||||||||||
| .overrideConfiguration(request.overrideConfiguration().orElse(null)) | ||||||||||||||
| .build(); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| public static CreateMultipartUploadRequest convertRequest(PutObjectRequest request) { | ||||||||||||||
|
|
||||||||||||||
| final CreateMultipartUploadRequest.Builder output = CreateMultipartUploadRequest.builder(); | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to include these lines in every file, they should not be deleted here.