All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy