/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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 org.apache.cayenne.crypto.transformer.bytes;
import java.io.UnsupportedEncodingException;
import org.apache.cayenne.crypto.CayenneCryptoException;
/**
* Represents a header with metadata about the encrypted data. A header is
* prependend to each encrypted value, and itself is not encrypted. Header
* format is the following:
* <ul>
* <li>byte 0..2: "magic" number identifying the format as Cayenne-crypto
* encrypted sequence.
* <li>byte 3: header length N, i.e. how many bytes the header contains,
* including magic number and the length indicator. N can be 0..127.
* <li>byte 4: a bit String representing various flags, such as compression.
* <li>byte 5..N: UTF8-encoded symbolic name of the encryption key.
* </ul>
*
* @since 4.0
*/
public class Header {
private static final String KEY_NAME_CHARSET = "UTF-8";
// "CC1" is a "magic number" identifying Cayenne-crypto version 1 value
private static final byte[] MAGIC_NUMBER = { 'C', 'C', '1' };
/**
* Position of the "flags" byte in the header.
*/
private static final int MAGIC_NUMBER_POSITION = 0;
/**
* Position of the header size byte in the header.
*/
private static final int SIZE_POSITION = 3;
/**
* Position of the "flags" byte in the header.
*/
private static final int FLAGS_POSITION = 4;
/**
* Position of the key name within the header block.
*/
private static final int KEY_NAME_OFFSET = 5;
/**
* Max size of a key name within a header.
*/
private static final int KEY_NAME_MAX_SIZE = Byte.MAX_VALUE - KEY_NAME_OFFSET;
/**
* A position of the compress bit.
*/
private static final int COMPRESS_BIT = 0;
/**
* A position if the HMAC bit
*/
private static final int HMAC_BIT = 1;
private byte[] data;
private int offset;
public static Header create(String keyName, boolean compressed, boolean withHMAC) {
byte[] keyNameBytes;
try {
keyNameBytes = keyName.getBytes(KEY_NAME_CHARSET);
} catch (UnsupportedEncodingException e) {
throw new CayenneCryptoException("Can't encode in " + KEY_NAME_CHARSET, e);
}
if (keyNameBytes.length > KEY_NAME_MAX_SIZE) {
throw new CayenneCryptoException("Key name '" + keyName
+ "' is too long. Its UTF8-encoded form should not exceed " + KEY_NAME_MAX_SIZE + " bytes");
}
int n = MAGIC_NUMBER.length + 1 + 1 + keyNameBytes.length;
byte[] data = new byte[n];
System.arraycopy(MAGIC_NUMBER, 0, data, MAGIC_NUMBER_POSITION, MAGIC_NUMBER.length);
// total header size
data[SIZE_POSITION] = (byte) n;
// flags
if (compressed) {
data[FLAGS_POSITION] = bitOn(data[FLAGS_POSITION], COMPRESS_BIT);
}
if (withHMAC) {
data[FLAGS_POSITION] = bitOn(data[FLAGS_POSITION], HMAC_BIT);
}
// key name
System.arraycopy(keyNameBytes, 0, data, KEY_NAME_OFFSET, keyNameBytes.length);
return create(data, 0);
}
public static Header create(byte[] data, int offset) {
return new Header(data, offset);
}
public static byte setCompressed(byte bits, boolean compressed) {
return compressed ? bitOn(bits, COMPRESS_BIT) : bitOff(bits, COMPRESS_BIT);
}
public static byte setHaveHMAC(byte bits, boolean haveHMAC) {
return haveHMAC ? bitOn(bits, HMAC_BIT) : bitOff(bits, HMAC_BIT);
}
private static byte bitOn(byte bits, int position) {
return (byte) (bits | (1 << position));
}
private static byte bitOff(byte bits, int position) {
return (byte) (bits & ~(1 << position));
}
private static boolean isBitOn(byte bits, int position) {
return ((bits >> position) & 1) == 1;
}
// private constructor... construction is done via factory methods...
private Header(byte[] data, int offset) {
this.data = data;
this.offset = offset;
}
public int size() {
return data[offset + SIZE_POSITION];
}
public boolean isCompressed() {
return isBitOn(getFlags(), COMPRESS_BIT);
}
public boolean haveHMAC() {
return isBitOn(getFlags(), HMAC_BIT);
}
public byte getFlags() {
return data[offset + FLAGS_POSITION];
}
/**
* Saves the header bytes in the provided buffer at specified offset.
*/
public void store(byte[] output, int outputOffset, byte flags) {
System.arraycopy(data, offset, output, outputOffset, size());
output[outputOffset + FLAGS_POSITION] = flags;
}
public String getKeyName() {
try {
return new String(data, offset + KEY_NAME_OFFSET, size() - KEY_NAME_OFFSET, KEY_NAME_CHARSET);
} catch (UnsupportedEncodingException e) {
throw new CayenneCryptoException("Can't decode with " + KEY_NAME_CHARSET, e);
}
}
}