com.amazonaws.encryptionsdk.internal.EncryptionContextSerializer Maven / Gradle / Ivy
/*
* Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
* in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.amazonaws.encryptionsdk.internal;
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* This class provides methods that serialize and deserialize the encryption context provided as a
* map containing key-value pairs comprised of strings.
*/
public class EncryptionContextSerializer {
private EncryptionContextSerializer() {
// Prevent instantiation
}
/**
* Serialize the encryption context provided as a map containing key-value pairs comprised of
* strings into a byte array.
*
* @param encryptionContext the map containing the encryption context to serialize.
* @return serialized bytes of the encryption context.
*/
public static byte[] serialize(Map encryptionContext) {
if (encryptionContext == null) return null;
if (encryptionContext.size() == 0) {
return new byte[0];
}
// Make sure we don't accidentally overwrite anything.
encryptionContext = Collections.unmodifiableMap(encryptionContext);
if (encryptionContext.size() > Short.MAX_VALUE) {
throw new AwsCryptoException(
"The number of entries in encryption context exceeds the allowed maximum "
+ Short.MAX_VALUE);
}
final ByteBuffer result = ByteBuffer.allocate(Short.MAX_VALUE);
result.order(ByteOrder.BIG_ENDIAN);
// write the number of key-value entries first
result.putShort((short) encryptionContext.size());
try {
final CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
// ensure all failures in encoder are reported.
encoder.onMalformedInput(CodingErrorAction.REPORT);
encoder.onUnmappableCharacter(CodingErrorAction.REPORT);
final SortedMap binaryEntries =
new TreeMap<>(new Utils.ComparingByteBuffers());
for (Entry mapEntry : encryptionContext.entrySet()) {
if (mapEntry.getKey() == null || mapEntry.getValue() == null) {
throw new AwsCryptoException(
"All keys and values in encryption context must be non-null.");
}
if (mapEntry.getKey().isEmpty() || mapEntry.getValue().isEmpty()) {
throw new AwsCryptoException(
"All keys and values in encryption context must be non-empty.");
}
final ByteBuffer keyBytes = encoder.encode(CharBuffer.wrap(mapEntry.getKey()));
final ByteBuffer valueBytes = encoder.encode(CharBuffer.wrap(mapEntry.getValue()));
// check for duplicate entries.
if (binaryEntries.put(keyBytes, valueBytes) != null) {
throw new AwsCryptoException("Encryption context contains duplicate entries.");
}
if (keyBytes.limit() > Short.MAX_VALUE || valueBytes.limit() > Short.MAX_VALUE) {
throw new AwsCryptoException(
"All keys and values in encryption context must be shorter than " + Short.MAX_VALUE);
}
}
for (final Entry entry : binaryEntries.entrySet()) {
// actual serialization happens here
result.putShort((short) entry.getKey().limit());
result.put(entry.getKey());
result.putShort((short) entry.getValue().limit());
result.put(entry.getValue());
}
// get and return the bytes that have been serialized
Utils.flip(result);
final byte[] encryptionContextBytes = new byte[result.limit()];
result.get(encryptionContextBytes);
return encryptionContextBytes;
} catch (CharacterCodingException e) {
throw new IllegalArgumentException(
"Encryption context contains an invalid unicode character");
} catch (BufferOverflowException e) {
throw new AwsCryptoException(
"The number of bytes in encryption context exceeds the allowed maximum "
+ Short.MAX_VALUE,
e);
}
}
/**
* Deserialize the provided byte array into a map containing key-value pairs comprised of strings.
*
* @param b the bytes to deserialize into a map representing the encryption context.
* @return the map containing key-value pairs comprised of strings.
*/
public static Map deserialize(final byte[] b) {
try {
if (b == null) {
return null;
}
if (b.length == 0) {
return (Collections.emptyMap());
}
final ByteBuffer encryptionContextBytes = ByteBuffer.wrap(b);
// retrieve the number of entries first
final int entryCount = encryptionContextBytes.getShort();
if (entryCount <= 0 || entryCount > Short.MAX_VALUE) {
throw new AwsCryptoException(
"The number of entries in encryption context must be greater than 0 and smaller than "
+ Short.MAX_VALUE);
}
final CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
// ensure all failures in decoder are reported.
decoder.onMalformedInput(CodingErrorAction.REPORT);
decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
final Map result = new HashMap<>(entryCount);
for (int i = 0; i < entryCount; i++) {
// retrieve key
final int keyLen = encryptionContextBytes.getShort();
if (keyLen <= 0 || keyLen > Short.MAX_VALUE) {
throw new AwsCryptoException(
"Key length must be greater than 0 and smaller than " + Short.MAX_VALUE);
}
final ByteBuffer keyBytes = encryptionContextBytes.slice();
Utils.limit(keyBytes, keyLen);
Utils.position(encryptionContextBytes, encryptionContextBytes.position() + keyLen);
final int valueLen = encryptionContextBytes.getShort();
if (valueLen <= 0 || valueLen > Short.MAX_VALUE) {
throw new AwsCryptoException(
"Value length must be greater than 0 and smaller than " + Short.MAX_VALUE);
}
// retrieve value
final ByteBuffer valueBytes = encryptionContextBytes.slice();
Utils.limit(valueBytes, valueLen);
Utils.position(encryptionContextBytes, encryptionContextBytes.position() + valueLen);
final CharBuffer keyChars = decoder.decode(keyBytes);
final CharBuffer valueChars = decoder.decode(valueBytes);
// check for duplicate entries.
if (result.put(keyChars.toString(), valueChars.toString()) != null) {
throw new AwsCryptoException("Encryption context contains duplicate entries.");
}
}
return result;
} catch (CharacterCodingException e) {
throw new IllegalArgumentException(
"Encryption context contains an invalid unicode character");
} catch (BufferUnderflowException e) {
throw new AwsCryptoException("Invalid encryption context. Expected more bytes.", e);
}
}
}