/* Copyright 2014 Duncan Jones
*
* Licensed 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.cryptonode.jncryptor;
import java.util.Arrays;
/**
* Base class for parsing and producing formatted ciphertext.
*/
abstract class AES256Ciphertext {
// Values are default protection to share with unit
// tests
static final int FLAG_PASSWORD = 0x01;
static final int ENCRYPTION_SALT_LENGTH = 8;
static final int HMAC_SALT_LENGTH = 8;
static final int AES_BLOCK_SIZE = 16;
static final int HMAC_SIZE = 32;
static final int HEADER_SIZE = 2;
static final int MINIMUM_LENGTH_WITH_PASSWORD = HEADER_SIZE
+ ENCRYPTION_SALT_LENGTH + HMAC_SALT_LENGTH + AES_BLOCK_SIZE + HMAC_SIZE;
static final int MINIMUM_LENGTH_WITHOUT_PASSWORD = HEADER_SIZE
+ AES_BLOCK_SIZE + HMAC_SIZE;
private final int version;
private final byte options;
private final byte[] encryptionSalt;
private final byte[] hmacSalt;
private final byte[] iv;
private final byte[] ciphertext;
private byte[] hmac;
private final boolean isPasswordBased;
/**
* Parses binary data to create an {@code AES256Ciphertext}.
*
* @param data
* the data to parse
* @throws InvalidDataException
* if the data is not valid
*/
AES256Ciphertext(byte[] data) throws InvalidDataException {
Validate.notNull(data, "Data cannot be null.");
// Need the header to be able to determine the length
if (data.length < HEADER_SIZE) {
throw new InvalidDataException("Not enough data to read header.");
}
int index = 0;
version = data[index++];
if (version != getVersionNumber()) {
throw new InvalidDataException(String.format(
"Expected version %d but found %d.", getVersionNumber(), version));
}
options = data[index++];
// Test for any invalid flags
if (options != 0x00 && options != FLAG_PASSWORD) {
throw new InvalidDataException("Unrecognised bit in the options byte.");
}
// If the password bit is set, we can expect salt values
isPasswordBased = ((options & FLAG_PASSWORD) == FLAG_PASSWORD);
final int minimumLength = (isPasswordBased) ? MINIMUM_LENGTH_WITH_PASSWORD
: MINIMUM_LENGTH_WITHOUT_PASSWORD;
if (data.length < minimumLength) {
throw new InvalidDataException(String.format(
"Data must be a minimum length of %d bytes, but found %d bytes.",
minimumLength, data.length));
}
final int ciphertextLength = data.length - minimumLength;
if (isPasswordBased) {
encryptionSalt = new byte[ENCRYPTION_SALT_LENGTH];
System.arraycopy(data, index, encryptionSalt, 0, encryptionSalt.length);
index += encryptionSalt.length;
hmacSalt = new byte[HMAC_SALT_LENGTH];
System.arraycopy(data, index, hmacSalt, 0, hmacSalt.length);
index += hmacSalt.length;
} else {
encryptionSalt = null;
hmacSalt = null;
}
iv = new byte[AES_BLOCK_SIZE];
System.arraycopy(data, index, iv, 0, iv.length);
index += iv.length;
ciphertext = new byte[ciphertextLength];
System.arraycopy(data, index, ciphertext, 0, ciphertextLength);
index += ciphertextLength;
hmac = new byte[HMAC_SIZE];
System.arraycopy(data, index, hmac, 0, hmac.length);
}
/**
* Constructs a {@code CryptorData} from its constituent parts. An
* {@code IllegalArgumentException} is thrown if any of the parameters are of
* the wrong length or invalid.
* <p>
* This constructor is used if the data was encrypted with a password.
*
* @param encryptionSalt
* the encryption salt
* @param hmacSalt
* the HMAC salt
* @param iv
* the initialisation value
* @param ciphertext
* the encrypted data
*/
AES256Ciphertext(byte[] encryptionSalt, byte[] hmacSalt, byte[] iv,
byte[] ciphertext) {
validateLength(encryptionSalt, "encryption salt", ENCRYPTION_SALT_LENGTH);
validateLength(hmacSalt, "HMAC salt", HMAC_SALT_LENGTH);
validateLength(iv, "IV", AES_BLOCK_SIZE);
this.version = getVersionNumber();
this.options = FLAG_PASSWORD;
this.encryptionSalt = encryptionSalt;
this.hmacSalt = hmacSalt;
this.iv = iv;
this.ciphertext = ciphertext;
this.isPasswordBased = true;
// HMAC will be set later
hmac = new byte[HMAC_SIZE];
}
/**
* Constructs a {@code CryptorData} from its constituent parts. An
* {@code IllegalArgumentException} is thrown if any of the parameters are of
* the wrong length or invalid.
* <p>
* This constructor is used if the data was encrypted with a key.
*
* @param iv
* the initialisation value
* @param ciphertext
* the encrypted data
*/
AES256Ciphertext(byte[] iv, byte[] ciphertext) {
validateLength(iv, "IV", AES_BLOCK_SIZE);
this.version = getVersionNumber();
this.options = 0;
this.iv = iv;
this.ciphertext = ciphertext;
this.encryptionSalt = null;
this.hmacSalt = null;
this.isPasswordBased = false;
// HMAC will be set later
hmac = new byte[HMAC_SIZE];
}
/**
* Checks the length of a byte array.
*
* @param data
* the data to check
* @param dataName
* the name of the field (to include in the exception)
* @param expectedLength
* the length the data should be
* @throws IllegalArgumentException
* if the data is not of the correct length
*/
private static void validateLength(byte[] data, String dataName,
int expectedLength) throws IllegalArgumentException {
if (data.length != expectedLength) {
throw new IllegalArgumentException(String.format(
"Invalid %s length. Expected %d bytes but found %d.", dataName,
expectedLength, data.length));
}
}
/**
* Returns the ciphertext, packaged as a byte array.
*
* @return the byte array
*/
byte[] getRawData() {
// Header: [Version | Options]
byte[] header = new byte[] { (byte) getVersionNumber(), 0 };
if (isPasswordBased) {
header[1] |= FLAG_PASSWORD;
}
// Pack result
final int dataSize;
if (isPasswordBased) {
dataSize = header.length + encryptionSalt.length + hmacSalt.length
+ iv.length + ciphertext.length + hmac.length;
} else {
dataSize = header.length + iv.length + ciphertext.length + hmac.length;
}
byte[] result = new byte[dataSize];
System.arraycopy(header, 0, result, 0, header.length);
if (isPasswordBased) {
System.arraycopy(encryptionSalt, 0, result, header.length,
encryptionSalt.length);
System.arraycopy(hmacSalt, 0, result, header.length
+ encryptionSalt.length, hmacSalt.length);
System.arraycopy(iv, 0, result, header.length + encryptionSalt.length
+ hmacSalt.length, iv.length);
System.arraycopy(ciphertext, 0, result, header.length
+ encryptionSalt.length + hmacSalt.length + iv.length,
ciphertext.length);
System.arraycopy(hmac, 0, result, header.length + encryptionSalt.length
+ hmacSalt.length + iv.length + ciphertext.length, hmac.length);
} else {
System.arraycopy(iv, 0, result, header.length, iv.length);
System.arraycopy(ciphertext, 0, result, header.length + iv.length,
ciphertext.length);
System.arraycopy(hmac, 0, result, header.length + iv.length
+ ciphertext.length, hmac.length);
}
return result;
}
/**
* @return the data to compute the HMAC over
*/
byte[] getDataToHMAC() {
byte[] rawData = getRawData();
byte[] result = new byte[rawData.length - HMAC_SIZE];
System.arraycopy(rawData, 0, result, 0, result.length);
return result;
}
/**
* @return the version
*/
int getVersion() {
return version;
}
/**
* @return the options
*/
byte getOptions() {
return options;
}
/**
* @return the encryptionSalt
*/
byte[] getEncryptionSalt() {
return encryptionSalt;
}
/**
* @return the hmacSalt
*/
byte[] getHmacSalt() {
return hmacSalt;
}
/**
* @return the iv
*/
byte[] getIv() {
return iv;
}
/**
* @return the ciphertext
*/
byte[] getCiphertext() {
return ciphertext;
}
/**
* @return the hmac
*/
byte[] getHmac() {
return hmac;
}
/**
* Indicates if the ciphertext was created using a password. If so, then the
* salt values will be present in the ciphertext.
*
* @return <code>true</code> if the ciphertext was created with a password
* (not a key), <code>false</code> otherwise
*/
public boolean isPasswordBased() {
return isPasswordBased;
}
/**
* @param hmac
* the hmac to set
*/
void setHmac(byte[] hmac) {
this.hmac = hmac;
}
/**
* @return the expected version number
*/
abstract int getVersionNumber();
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(ciphertext);
result = prime * result + Arrays.hashCode(encryptionSalt);
result = prime * result + Arrays.hashCode(hmac);
result = prime * result + Arrays.hashCode(hmacSalt);
result = prime * result + (isPasswordBased ? 1231 : 1237);
result = prime * result + Arrays.hashCode(iv);
result = prime * result + options;
result = prime * result + version;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AES256Ciphertext other = (AES256Ciphertext) obj;
if (!Arrays.equals(ciphertext, other.ciphertext)) {
return false;
}
if (!Arrays.equals(encryptionSalt, other.encryptionSalt)) {
return false;
}
if (!Arrays.equals(hmac, other.hmac)) {
return false;
}
if (!Arrays.equals(hmacSalt, other.hmacSalt)) {
return false;
}
if (isPasswordBased != other.isPasswordBased) {
return false;
}
if (!Arrays.equals(iv, other.iv)) {
return false;
}
if (options != other.options) {
return false;
}
if (version != other.version) {
return false;
}
return true;
}
}