/* See LICENSE for licensing and NOTICE for copyright. */ package org.cryptacular; import java.io.IOException; import java.io.InputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import org.cryptacular.util.ByteUtil; /** * Cleartext header prepended to ciphertext providing data required for decryption. * * <p>Data format:</p> * * <pre> +-----+----------+-------+------------+---------+ | Len | NonceLen | Nonce | KeyNameLen | KeyName | +-----+----------+-------+------------+---------+ * </pre> * * <p>Where fields are defined as follows:</p> * * <ul> * <li>Len - Total header length in bytes (4-byte integer)</li> * <li>NonceLen - Nonce length in bytes (4-byte integer)</li> * <li>Nonce - Nonce bytes (variable length)</li> * <li>KeyNameLen (OPTIONAL) - Key name length in bytes (4-byte integer)</li> * <li>KeyName (OPTIONAL) - Key name encoded as bytes in platform-specific encoding (variable length)</li> * </ul> * * <p>The last two fields are optional and provide support for multiple keys at the encryption provider. A common case * for multiple keys is key rotation; by tagging encrypted data with a key name, an old key may be retrieved by name to * decrypt outstanding data which will be subsequently re-encrypted with a new key.</p> * * @author Middleware Services */ public class CiphertextHeader { /** Header nonce field value. */ private final byte[] nonce; /** Header key name field value. */ private String keyName; /** Header length in bytes. */ private int length; /** * Creates a new instance with only a nonce. * * @param nonce Nonce bytes. */ public CiphertextHeader(final byte[] nonce) { this(nonce, null); } /** * Creates a new instance with a nonce and named key. * * @param nonce Nonce bytes. * @param keyName Key name. */ public CiphertextHeader(final byte[] nonce, final String keyName) { this.nonce = nonce; this.length = 8 + nonce.length; if (keyName != null) { this.length += 4 + keyName.getBytes().length; this.keyName = keyName; } } /** * Gets the header length in bytes. * * @return Header length in bytes. */ public int getLength() { return this.length; } /** * Gets the bytes of the nonce/IV. * * @return Nonce bytes. */ public byte[] getNonce() { return this.nonce; } /** * Gets the encryption key name stored in the header. * * @return Encryption key name. */ public String getKeyName() { return this.keyName; } /** * Encodes the header into bytes. * * @return Byte representation of header. */ public byte[] encode() { final ByteBuffer bb = ByteBuffer.allocate(length); bb.order(ByteOrder.BIG_ENDIAN); bb.putInt(length); bb.putInt(nonce.length); bb.put(nonce); if (keyName != null) { final byte[] b = keyName.getBytes(); bb.putInt(b.length); bb.put(b); } return bb.array(); } /** * Creates a header from encrypted data containing a cleartext header prepended to the start. * * @param data Encrypted data with prepended header data. * * @return Decoded header. * * @throws EncodingException when ciphertext header cannot be decoded. */ public static CiphertextHeader decode(final byte[] data) throws EncodingException { final ByteBuffer bb = ByteBuffer.wrap(data); bb.order(ByteOrder.BIG_ENDIAN); final int length = bb.getInt(); if (length < 0) { throw new EncodingException("Invalid ciphertext header length: " + length); } final byte[] nonce; int nonceLen = 0; try { nonceLen = bb.getInt(); nonce = new byte[nonceLen]; bb.get(nonce); } catch (IndexOutOfBoundsException | BufferUnderflowException e) { throw new EncodingException("Invalid nonce length: " + nonceLen); } String keyName = null; if (length > nonce.length + 8) { final byte[] b; int keyLen = 0; try { keyLen = bb.getInt(); b = new byte[keyLen]; bb.get(b); keyName = new String(b); } catch (IndexOutOfBoundsException | BufferUnderflowException e) { throw new EncodingException("Invalid key length: " + keyLen); } } return new CiphertextHeader(nonce, keyName); } /** * Creates a header from encrypted data containing a cleartext header prepended to the start. * * @param input Input stream that is positioned at the start of ciphertext header data. * * @return Decoded header. * * @throws EncodingException when ciphertext header cannot be decoded. * @throws StreamException on stream IO errors. */ public static CiphertextHeader decode(final InputStream input) throws EncodingException, StreamException { final int length = ByteUtil.readInt(input); if (length < 0) { throw new EncodingException("Invalid ciphertext header length: " + length); } final byte[] nonce; int nonceLen = 0; try { nonceLen = ByteUtil.readInt(input); nonce = new byte[nonceLen]; input.read(nonce); } catch (ArrayIndexOutOfBoundsException e) { throw new EncodingException("Invalid nonce length: " + nonceLen); } catch (IOException e) { throw new StreamException(e); } String keyName = null; if (length > nonce.length + 8) { final byte[] b; int keyLen = 0; try { keyLen = ByteUtil.readInt(input); b = new byte[keyLen]; input.read(b); } catch (ArrayIndexOutOfBoundsException e) { throw new EncodingException("Invalid key length: " + keyLen); } catch (IOException e) { throw new StreamException(e); } keyName = new String(b); } return new CiphertextHeader(nonce, keyName); } }