1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.services.s3.internal.checksums; 17 18 import static software.amazon.awssdk.services.s3.model.ServerSideEncryption.AWS_KMS; 19 20 import java.util.Arrays; 21 import java.util.Optional; 22 import software.amazon.awssdk.annotations.SdkInternalApi; 23 import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; 24 import software.amazon.awssdk.core.ClientType; 25 import software.amazon.awssdk.core.SdkRequest; 26 import software.amazon.awssdk.core.checksums.ChecksumSpecs; 27 import software.amazon.awssdk.core.checksums.SdkChecksum; 28 import software.amazon.awssdk.core.exception.RetryableException; 29 import software.amazon.awssdk.core.interceptor.ExecutionAttribute; 30 import software.amazon.awssdk.core.interceptor.ExecutionAttributes; 31 import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; 32 import software.amazon.awssdk.http.SdkHttpHeaders; 33 import software.amazon.awssdk.http.SdkHttpRequest; 34 import software.amazon.awssdk.http.SdkHttpResponse; 35 import software.amazon.awssdk.services.s3.S3Client; 36 import software.amazon.awssdk.services.s3.S3Configuration; 37 import software.amazon.awssdk.services.s3.internal.handlers.AsyncChecksumValidationInterceptor; 38 import software.amazon.awssdk.services.s3.internal.handlers.SyncChecksumValidationInterceptor; 39 import software.amazon.awssdk.services.s3.model.ChecksumMode; 40 import software.amazon.awssdk.services.s3.model.GetObjectRequest; 41 import software.amazon.awssdk.services.s3.model.PutObjectRequest; 42 import software.amazon.awssdk.services.s3.model.PutObjectResponse; 43 import software.amazon.awssdk.utils.BinaryUtils; 44 import software.amazon.awssdk.utils.StringUtils; 45 import software.amazon.awssdk.utils.internal.Base16Lower; 46 47 /** 48 * Class used by {@link SyncChecksumValidationInterceptor} and 49 * {@link AsyncChecksumValidationInterceptor} to determine if trailing checksums 50 * should be enabled for a given request. 51 */ 52 @SdkInternalApi 53 public final class ChecksumsEnabledValidator { 54 55 public static final ExecutionAttribute<SdkChecksum> CHECKSUM = new ExecutionAttribute<>("checksum"); 56 ChecksumsEnabledValidator()57 private ChecksumsEnabledValidator() { 58 } 59 60 /** 61 * Checks if trailing checksum is enabled and {@link ChecksumMode} is disabled for 62 * {@link S3Client#getObject(GetObjectRequest)} per request. 63 * 64 * @param request the request 65 * @param executionAttributes the executionAttributes 66 * @return true if trailing checksums is enabled and ChecksumMode is disabled, false otherwise 67 */ getObjectChecksumEnabledPerRequest(SdkRequest request, ExecutionAttributes executionAttributes)68 public static boolean getObjectChecksumEnabledPerRequest(SdkRequest request, 69 ExecutionAttributes executionAttributes) { 70 return request instanceof GetObjectRequest 71 && ((GetObjectRequest) request).checksumMode() != ChecksumMode.ENABLED 72 && checksumEnabledPerConfig(executionAttributes); 73 } 74 75 /** 76 * Checks if trailing checksum is enabled for {@link S3Client#getObject(GetObjectRequest)} per response. 77 * 78 * @param request the request 79 * @param responseHeaders the response headers 80 * @return true if trailing checksums is enabled, false otherwise 81 */ getObjectChecksumEnabledPerResponse(SdkRequest request, SdkHttpHeaders responseHeaders)82 public static boolean getObjectChecksumEnabledPerResponse(SdkRequest request, SdkHttpHeaders responseHeaders) { 83 return request instanceof GetObjectRequest && checksumEnabledPerResponse(responseHeaders); 84 } 85 86 /** 87 * Validates that checksums should be enabled based on {@link ClientType} and the presence of S3 specific headers. 88 * 89 * @param expectedClientType - The expected client type for enabling checksums 90 * @param executionAttributes - {@link ExecutionAttributes} to determine the actual client type 91 * @return If trailing checksums should be enabled for this request. 92 */ shouldRecordChecksum(SdkRequest sdkRequest, ClientType expectedClientType, ExecutionAttributes executionAttributes, SdkHttpRequest httpRequest)93 public static boolean shouldRecordChecksum(SdkRequest sdkRequest, 94 ClientType expectedClientType, 95 ExecutionAttributes executionAttributes, 96 SdkHttpRequest httpRequest) { 97 if (!(sdkRequest instanceof PutObjectRequest)) { 98 return false; 99 } 100 101 ClientType actualClientType = executionAttributes.getAttribute(SdkExecutionAttribute.CLIENT_TYPE); 102 103 if (expectedClientType != actualClientType) { 104 return false; 105 } 106 107 108 if (hasServerSideEncryptionHeader(httpRequest)) { 109 return false; 110 } 111 112 //Checksum validation is done at Service side when HTTP Checksum algorithm attribute is set. 113 if (isHttpCheckSumValidationEnabled(executionAttributes)) { 114 return false; 115 } 116 117 return checksumEnabledPerConfig(executionAttributes); 118 } 119 isHttpCheckSumValidationEnabled(ExecutionAttributes executionAttributes)120 private static boolean isHttpCheckSumValidationEnabled(ExecutionAttributes executionAttributes) { 121 Optional<ChecksumSpecs> resolvedChecksum = 122 executionAttributes.getOptionalAttribute(SdkExecutionAttribute.RESOLVED_CHECKSUM_SPECS); 123 if (resolvedChecksum.isPresent()) { 124 ChecksumSpecs checksumSpecs = resolvedChecksum.get(); 125 return checksumSpecs.algorithm() != null; 126 } 127 return false; 128 } 129 responseChecksumIsValid(SdkHttpResponse httpResponse)130 public static boolean responseChecksumIsValid(SdkHttpResponse httpResponse) { 131 return !hasServerSideEncryptionHeader(httpResponse); 132 } 133 hasServerSideEncryptionHeader(SdkHttpHeaders httpRequest)134 private static boolean hasServerSideEncryptionHeader(SdkHttpHeaders httpRequest) { 135 // S3 doesn't support trailing checksums for customer encryption 136 if (httpRequest.firstMatchingHeader(ChecksumConstant.SERVER_SIDE_CUSTOMER_ENCRYPTION_HEADER).isPresent()) { 137 return true; 138 } 139 140 // S3 doesn't support trailing checksums for KMS encrypted objects 141 if (httpRequest.firstMatchingHeader(ChecksumConstant.SERVER_SIDE_ENCRYPTION_HEADER) 142 .filter(h -> h.contains(AWS_KMS.toString())) 143 .isPresent()) { 144 return true; 145 } 146 return false; 147 } 148 149 /** 150 * Client side validation for {@link PutObjectRequest} 151 * 152 * @param response the response 153 * @param executionAttributes the execution attributes 154 */ validatePutObjectChecksum(PutObjectResponse response, ExecutionAttributes executionAttributes)155 public static void validatePutObjectChecksum(PutObjectResponse response, ExecutionAttributes executionAttributes) { 156 SdkChecksum checksum = executionAttributes.getAttribute(CHECKSUM); 157 158 if (response.eTag() != null) { 159 String contentMd5 = BinaryUtils.toBase64(checksum.getChecksumBytes()); 160 byte[] digest = BinaryUtils.fromBase64(contentMd5); 161 byte[] ssHash = Base16Lower.decode(StringUtils.replace(response.eTag(), "\"", "")); 162 163 if (!Arrays.equals(digest, ssHash)) { 164 throw RetryableException.create( 165 String.format("Data read has a different checksum than expected. Was 0x%s, but expected 0x%s. " + 166 "This commonly means that the data was corrupted between the client and " + 167 "service. Note: Despite this error, the upload still completed and was persisted in S3.", 168 BinaryUtils.toHex(digest), BinaryUtils.toHex(ssHash))); 169 } 170 } 171 } 172 173 /** 174 * Check the response header to see if the trailing checksum is enabled. 175 * 176 * @param responseHeaders the SdkHttpHeaders 177 * @return true if the trailing checksum is present in the header, false otherwise. 178 */ checksumEnabledPerResponse(SdkHttpHeaders responseHeaders)179 private static boolean checksumEnabledPerResponse(SdkHttpHeaders responseHeaders) { 180 return responseHeaders.firstMatchingHeader(ChecksumConstant.CHECKSUM_ENABLED_RESPONSE_HEADER) 181 .filter(b -> b.equals(ChecksumConstant.ENABLE_MD5_CHECKSUM_HEADER_VALUE)) 182 .isPresent(); 183 } 184 185 /** 186 * Check the {@link S3Configuration#checksumValidationEnabled()} to see if the checksum is enabled. 187 * 188 * @param executionAttributes the execution attributes 189 * @return true if the trailing checksum is enabled in the config, false otherwise. 190 */ checksumEnabledPerConfig(ExecutionAttributes executionAttributes)191 private static boolean checksumEnabledPerConfig(ExecutionAttributes executionAttributes) { 192 S3Configuration serviceConfiguration = 193 (S3Configuration) executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_CONFIG); 194 195 return serviceConfiguration == null || serviceConfiguration.checksumValidationEnabled(); 196 } 197 } 198