|
2 | 2 | // SPDX-License-Identifier: Apache-2.0
|
3 | 3 | package software.amazon.encryption.s3.internal;
|
4 | 4 |
|
| 5 | +import com.sun.xml.messaging.saaj.packaging.mime.internet.MimeUtility; |
5 | 6 | import software.amazon.awssdk.core.ResponseInputStream;
|
6 | 7 | import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
7 | 8 | import software.amazon.awssdk.protocols.jsoncore.JsonNode;
|
|
15 | 16 | import software.amazon.encryption.s3.materials.EncryptedDataKey;
|
16 | 17 | import software.amazon.encryption.s3.materials.S3Keyring;
|
17 | 18 |
|
| 19 | +import java.io.ByteArrayOutputStream; |
| 20 | +import java.io.DataOutputStream; |
| 21 | +import java.io.IOException; |
| 22 | +import java.io.UnsupportedEncodingException; |
18 | 23 | import java.nio.charset.StandardCharsets;
|
19 | 24 | import java.util.Base64;
|
20 | 25 | import java.util.HashMap;
|
@@ -135,9 +140,13 @@ private ContentMetadata readFromMap(Map<String, String> metadata, GetObjectRespo
|
135 | 140 | // Get encrypted data key encryption context
|
136 | 141 | final Map<String, String> encryptionContext = new HashMap<>();
|
137 | 142 | final String jsonEncryptionContext = metadata.get(MetadataKeyConstants.ENCRYPTED_DATA_KEY_CONTEXT);
|
| 143 | + // When the encryption context contains non-US-ASCII characters, |
| 144 | + // the S3 server applies an esoteric encoding to the object metadata. |
| 145 | + // Reverse that, to allow decryption. |
| 146 | + final String decodedJsonEncryptionContext = decodeS3CustomEncoding(jsonEncryptionContext); |
138 | 147 | try {
|
139 | 148 | JsonNodeParser parser = JsonNodeParser.create();
|
140 |
| - JsonNode objectNode = parser.parse(jsonEncryptionContext); |
| 149 | + JsonNode objectNode = parser.parse(decodedJsonEncryptionContext); |
141 | 150 |
|
142 | 151 | for (Map.Entry<String, JsonNode> entry : objectNode.asObject().entrySet()) {
|
143 | 152 | encryptionContext.put(entry.getKey(), entry.getValue().asString());
|
@@ -171,6 +180,46 @@ public ContentMetadata decode(GetObjectRequest request, GetObjectResponse respon
|
171 | 180 | }
|
172 | 181 | }
|
173 | 182 |
|
| 183 | + private static String decodeS3CustomEncoding(final String s) { |
| 184 | + final String mimeDecoded; |
| 185 | + try { |
| 186 | + mimeDecoded = MimeUtility.decodeText(s); |
| 187 | + } catch (UnsupportedEncodingException ex) { |
| 188 | + throw new S3EncryptionClientException("Unable to decode S3 object metadata: " + s, ex); |
| 189 | + } |
| 190 | + // Once MIME decoded, we need to recover the correct code points from the second encoding pass |
| 191 | + // Otherwise, decryption fails |
| 192 | + try { |
| 193 | + final StringBuilder stringBuilder = new StringBuilder(); |
| 194 | + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| 195 | + final DataOutputStream out = new DataOutputStream(baos); |
| 196 | + final byte[] sInBytes = mimeDecoded.getBytes(StandardCharsets.UTF_8); |
| 197 | + final char[] sInChars = mimeDecoded.toCharArray(); |
| 198 | + |
| 199 | + int nonAsciiChars = 0; |
| 200 | + for (int i = 0; i < sInChars.length; i++) { |
| 201 | + if (sInChars[i] > 127) { |
| 202 | + byte[] buf = {sInBytes[i + nonAsciiChars], sInBytes[i + nonAsciiChars + 1]}; |
| 203 | + // temporarily re-encode as UTF-8 |
| 204 | + String wrongString = new String(buf, StandardCharsets.UTF_8); |
| 205 | + // write its code point |
| 206 | + out.write(wrongString.charAt(0)); |
| 207 | + nonAsciiChars++; |
| 208 | + } else { |
| 209 | + if (baos.size() > 0) { |
| 210 | + // This is not the most efficient, but we prefer to specify UTF_8 |
| 211 | + stringBuilder.append(new String(baos.toByteArray(), StandardCharsets.UTF_8)); |
| 212 | + baos.reset(); |
| 213 | + } |
| 214 | + stringBuilder.append(sInChars[i]); |
| 215 | + } |
| 216 | + } |
| 217 | + return stringBuilder.toString(); |
| 218 | + } catch (IOException exception) { |
| 219 | + throw new S3EncryptionClientException("Unable to decode S3 object metadata: " + s, exception); |
| 220 | + } |
| 221 | + } |
| 222 | + |
174 | 223 | private ContentMetadata decodeFromObjectMetadata(GetObjectRequest request, GetObjectResponse response) {
|
175 | 224 | return readFromMap(response.metadata(), response);
|
176 | 225 | }
|
|
0 commit comments