org.owasp.esapi.crypto.CipherText Maven / Gradle / Ivy
Show all versions of esapi Show documentation
/*
* OWASP Enterprise Security API (ESAPI)
*
* This file is part of the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. For details, please see
* http://www.owasp.org/index.php/ESAPI.
*
* Copyright © 2009 - The OWASP Foundation
*/
package org.owasp.esapi.crypto;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.EnumSet;
import java.util.Iterator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Encryptor;
import org.owasp.esapi.Logger;
import org.owasp.esapi.errors.EncryptionException;
import org.owasp.esapi.errors.EnterpriseSecurityRuntimeException;
// CHECKME: Some of these assertions probably should be actual runtime checks
// with suitable exceptions to account for cases where programmers
// accidentally pass in byte arrays that are not really serialized
// CipherText objects (note: as per asPortableSerializedByteArra()).
// However, not sure what exception time is really suitable here.
// It probably should be a sub-class of RuntimeException, but
// IllegalArguementException doesn't really make sense here. Suggestions?
/**
* A {@code Serializable} interface representing the result of encrypting
* plaintext and some additional information about the encryption algorithm,
* the IV (if pertinent), and an optional Message Authentication Code (MAC).
*
* Note that while this class is {@code Serializable} in the usual Java sense,
* ESAPI uses {@link #asPortableSerializedByteArray()} for serialization. Not
* only is this serialization somewhat more compact, it is also portable
* across other ESAPI programming language implementations. However, Java
* serialization is supported in the event that one wishes to store
* {@code CipherText} in an {@code HttpSession} object.
*
* Copyright © 2009 - The OWASP Foundation
*
* @author [email protected]
* @see PlainText
* @see org.owasp.esapi.Encryptor
* @since 2.0
*/
public final class CipherText implements Serializable {
// NOTE: Do NOT change this in future versions, unless you are knowingly
// making changes to the class that will render this class incompatible
// with previously serialized objects from older versions of this class.
// If this is done, that you must provide for supporting earlier ESAPI versions.
// Be wary making incompatible changes as discussed at:
// http://java.sun.com/javase/6/docs/platform/serialization/spec/version.html#6678
// Any incompatible change in the serialization of CipherText *must* be
// reflected in the class CipherTextSerializer.
// This should be *same* version as in CipherTextSerializer and KeyDerivationFunction.
// If one changes, the other should as well to accommodate any differences.
// Previous versions: 20110203 - Original version (ESAPI releases 2.0 & 2.0.1)
// 20130830 - Fix to issue #306 (release 2.1.0)
public static final int cipherTextVersion = 20130830; // Format: YYYYMMDD, max is 99991231.
// Required by Serializable classes.
private static final long serialVersionUID = cipherTextVersion; // Format: YYYYMMDD
private static final Logger logger = ESAPI.getLogger("CipherText");
private CipherSpec cipherSpec_ = null;
private byte[] raw_ciphertext_ = null;
private byte[] separate_mac_ = null;
private long encryption_timestamp_ = 0;
private int kdfVersion_ = KeyDerivationFunction.kdfVersion;
private int kdfPrfSelection_ = KeyDerivationFunction.getDefaultPRFSelection();
// All the various pieces that can be set, either directly or indirectly
// via CipherSpec.
private enum CipherTextFlags {
ALGNAME, CIPHERMODE, PADDING, KEYSIZE, BLOCKSIZE, CIPHERTEXT, INITVECTOR
}
// If we have everything set, we compare it to this using '==' which javac
// specially overloads for this.
private final EnumSet allCtFlags =
EnumSet.of(CipherTextFlags.ALGNAME, CipherTextFlags.CIPHERMODE,
CipherTextFlags.PADDING, CipherTextFlags.KEYSIZE,
CipherTextFlags.BLOCKSIZE, CipherTextFlags.CIPHERTEXT,
CipherTextFlags.INITVECTOR);
// These are all the pieces we collect when passed a CipherSpec object.
private final EnumSet fromCipherSpec =
EnumSet.of(CipherTextFlags.ALGNAME, CipherTextFlags.CIPHERMODE,
CipherTextFlags.PADDING, CipherTextFlags.KEYSIZE,
CipherTextFlags.BLOCKSIZE);
// How much we've collected so far. We start out with having collected nothing.
private EnumSet progress = EnumSet.noneOf(CipherTextFlags.class);
// Check if versions of KeyDerivationFunction, CipherText, and
// CipherTextSerializer are all the same.
{
// Ignore error about comparing identical versions and dead code.
// We expect them to be, but the point is to catch us if they aren't.
if ( CipherTextSerializer.cipherTextSerializerVersion != CipherText.cipherTextVersion ) {
throw new ExceptionInInitializerError("Versions of CipherTextSerializer and CipherText are not compatible.");
}
if ( CipherTextSerializer.cipherTextSerializerVersion != KeyDerivationFunction.kdfVersion ) {
throw new ExceptionInInitializerError("Versions of CipherTextSerializer and KeyDerivationFunction are not compatible.");
}
}
/////////////////////////// C O N S T R U C T O R S /////////////////////////
/**
* Default CTOR. Takes all the defaults from the ESAPI.properties, or
* default values from initial values from this class (when appropriate)
* when they are not set in ESAPI.properties.
*/
public CipherText() {
cipherSpec_ = new CipherSpec(); // Uses default for everything but IV.
received(fromCipherSpec);
}
/**
* Construct from a {@code CipherSpec} object. Still needs to have
* {@link #setCiphertext(byte[])} or {@link #setIVandCiphertext(byte[], byte[])}
* called to be usable.
*
* @param cipherSpec The cipher specification to use.
*/
public CipherText(final CipherSpec cipherSpec) {
cipherSpec_ = cipherSpec;
received(fromCipherSpec);
if ( cipherSpec.getIV() != null ) {
received(CipherTextFlags.INITVECTOR);
}
}
/**
* Construct from a {@code CipherSpec} object and the raw ciphertext.
*
* @param cipherSpec The cipher specification to use.
* @param cipherText The raw ciphertext bytes to use.
* @throws EncryptionException Thrown if {@code cipherText} is null or
* empty array.
*/
public CipherText(final CipherSpec cipherSpec, byte[] cipherText)
throws EncryptionException
{
cipherSpec_ = cipherSpec;
setCiphertext(cipherText);
received(fromCipherSpec);
if ( cipherSpec.getIV() != null ) {
received(CipherTextFlags.INITVECTOR);
}
}
/** Create a {@code CipherText} object from what is supposed to be a
* portable serialized byte array, given in network byte order, that
* represents a valid, previously serialized {@code CipherText} object
* using {@link #asPortableSerializedByteArray()}.
* @param bytes A byte array created via
* {@code CipherText.asPortableSerializedByteArray()}
* @return A {@code CipherText} object reconstructed from the byte array.
* @throws EncryptionException
* @see #asPortableSerializedByteArray()
*/ // DISCUSS: BTW, I detest this name. Suggestions???
public static CipherText fromPortableSerializedBytes(byte[] bytes)
throws EncryptionException
{
CipherTextSerializer cts = new CipherTextSerializer(bytes);
return cts.asCipherText();
}
///////////////////////// P U B L I C M E T H O D S ////////////////////
/**
* Obtain the String representing the cipher transformation used to encrypt
* the plaintext. The cipher transformation represents the cipher algorithm,
* the cipher mode, and the padding scheme used to do the encryption. An
* example would be "AES/CBC/PKCS5Padding". See Appendix A in the
*
* Java Cryptography Architecture Reference Guide
* for information about standard supported cipher transformation names.
*
* The cipher transformation name is usually sufficient to be passed to
* {@link javax.crypto.Cipher#getInstance(String)} to create a
* Cipher
object to decrypt the ciphertext.
*
* @return The cipher transformation name used to encrypt the plaintext
* resulting in this ciphertext.
*/
public String getCipherTransformation() {
return cipherSpec_.getCipherTransformation();
}
/**
* Obtain the name of the cipher algorithm used for encrypting the
* plaintext.
*
* @return The name as the cryptographic algorithm used to perform the
* encryption resulting in this ciphertext.
*/
public String getCipherAlgorithm() {
return cipherSpec_.getCipherAlgorithm();
}
/**
* Retrieve the key size used with the cipher algorithm that was used to
* encrypt data to produce this ciphertext.
*
* @return The key size in bits. We work in bits because that's the crypto way!
*/
public int getKeySize() {
return cipherSpec_.getKeySize();
}
/**
* Retrieve the block size (in bytes!) of the cipher used for encryption.
* (Note: If an IV is used, this will also be the IV length.)
*
* @return The block size in bytes. (Bits, bytes! It's confusing I know. Blame
* the cryptographers; we've just following
* convention.)
*/
public int getBlockSize() {
return cipherSpec_.getBlockSize();
}
/**
* Get the name of the cipher mode used to encrypt some plaintext.
*
* @return The name of the cipher mode used to encrypt the plaintext
* resulting in this ciphertext. E.g., "CBC" for "cipher block
* chaining", "ECB" for "electronic code book", etc.
*/
public String getCipherMode() {
return cipherSpec_.getCipherMode();
}
/**
* Get the name of the padding scheme used to encrypt some plaintext.
*
* @return The name of the padding scheme used to encrypt the plaintext
* resulting in this ciphertext. Example: "PKCS5Padding". If no
* padding was used "None" is returned.
*/
public String getPaddingScheme() {
return cipherSpec_.getPaddingScheme();
}
/**
* Return the initialization vector (IV) used to encrypt the plaintext
* if applicable.
*
* @return The IV is returned if the cipher mode used to encrypt the
* plaintext was not "ECB". ECB mode does not use an IV so in
* that case, null
is returned.
*/
public byte[] getIV() {
if ( isCollected(CipherTextFlags.INITVECTOR) ) {
return cipherSpec_.getIV();
} else {
logger.error(Logger.SECURITY_FAILURE, "IV not set yet; unable to retrieve; returning null");
return null;
}
}
/**
* Return true if the cipher mode used requires an IV. Usually this will
* be true unless ECB mode (which should be avoided whenever possible) is
* used.
*/
public boolean requiresIV() {
return cipherSpec_.requiresIV();
}
/**
* Get the raw ciphertext byte array resulting from encrypting some
* plaintext.
*
* @return A copy of the raw ciphertext as a byte array.
*/
public byte[] getRawCipherText() {
if ( isCollected(CipherTextFlags.CIPHERTEXT) ) {
byte[] copy = new byte[ raw_ciphertext_.length ];
System.arraycopy(raw_ciphertext_, 0, copy, 0, raw_ciphertext_.length);
return copy;
} else {
logger.error(Logger.SECURITY_FAILURE, "Raw ciphertext not set yet; unable to retrieve; returning null");
return null;
}
}
/**
* Get number of bytes in raw ciphertext. Zero is returned if ciphertext has not
* yet been stored.
*
* @return The number of bytes of raw ciphertext; 0 if no raw ciphertext has been stored.
*/
public int getRawCipherTextByteLength() {
if ( raw_ciphertext_ != null ) {
return raw_ciphertext_.length;
} else {
return 0;
}
}
/**
* Return a base64-encoded representation of the raw ciphertext alone. Even
* in the case where an IV is used, the IV is not prepended before the
* base64-encoding is performed.
*
* If there is a need to store an encrypted value, say in a database, this
* is not the method you should use unless you are using a fixed
* IV or are planning on retrieving the IV and storing it somewhere separately
* (e.g., a different database column). If you are not using a fixed IV
* (which is highly discouraged), you should normally use
* {@link #getEncodedIVCipherText()} instead.
*
* @see #getEncodedIVCipherText()
*/
public String getBase64EncodedRawCipherText() {
return ESAPI.encoder().encodeForBase64(getRawCipherText(),false);
}
/**
* Return the ciphertext as a base64-encoded String
. If an
* IV was used, the IV if first prepended to the raw ciphertext before
* base64-encoding. If an IV is not used, then this method returns the same
* value as {@link #getBase64EncodedRawCipherText()}.
*
* Generally, this is the method that you should use unless you only
* are using a fixed IV and a storing that IV separately, in which case
* using {@link #getBase64EncodedRawCipherText()} can reduce the storage
* overhead.
*
* @return The base64-encoded ciphertext or base64-encoded IV + ciphertext.
* @see #getBase64EncodedRawCipherText()
*/
public String getEncodedIVCipherText() {
if ( isCollected(CipherTextFlags.INITVECTOR) && isCollected(CipherTextFlags.CIPHERTEXT) ) {
// First concatenate IV + raw ciphertext
byte[] iv = getIV();
byte[] raw = getRawCipherText();
byte[] ivPlusCipherText = new byte[ iv.length + raw.length ];
System.arraycopy(iv, 0, ivPlusCipherText, 0, iv.length);
System.arraycopy(raw, 0, ivPlusCipherText, iv.length, raw.length);
// Then return the base64 encoded result
return ESAPI.encoder().encodeForBase64(ivPlusCipherText, false);
} else {
logger.error(Logger.SECURITY_FAILURE, "Raw ciphertext and/or IV not set yet; unable to retrieve; returning null");
return null;
}
}
/**
* Compute and store the Message Authentication Code (MAC) if the ESAPI property
* {@code Encryptor.CipherText.useMAC} is set to {@code true}. If it
* is, the MAC is conceptually calculated as:
*
* authKey = DerivedKey(secret_key, "authenticate")
* HMAC-SHA1(authKey, IV + secret_key)
*
* where derived key is an HMacSHA1, possibly repeated multiple times.
* (See {@link org.owasp.esapi.crypto.CryptoHelper#computeDerivedKey(SecretKey, int, String)}
* for details.)
*
* Perceived Benefits: There are certain cases where if an attacker
* is able to change the IV. When one uses a authenticity key that is
* derived from the "master" key, it also makes it possible to know when
* the incorrect key was attempted to be used to decrypt the ciphertext.
*
* NOTE: The purpose of this MAC (which is always computed by the
* ESAPI reference model implementing {@code Encryptor}) is to ensure the
* authenticity of the IV and ciphertext. Among other things, this prevents
* an adversary from substituting the IV with one of their own choosing.
* Because we don't know whether or not the recipient of this {@code CipherText}
* object will want to validate the authenticity or not, the reference
* implementation of {@code Encryptor} always computes it and includes it.
* The recipient of the ciphertext can then choose whether or not to validate
* it.
*
* @param authKey The secret key that is used for proving authenticity of
* the IV and ciphertext. This key should be derived from
* the {@code SecretKey} passed to the
* {@link Encryptor#encrypt(javax.crypto.SecretKey, PlainText)}
* and
* {@link Encryptor#decrypt(javax.crypto.SecretKey, CipherText)}
* methods or the "master" key when those corresponding
* encrypt / decrypt methods are used. This authenticity key
* should be the same length and for the same cipher algorithm
* as this {@code SecretKey}. The method
* {@link org.owasp.esapi.crypto.CryptoHelper#computeDerivedKey(SecretKey, int, String)}
* is a secure way to produce this derived key.
*/ // DISCUSS - Cryptographers David Wagner, Ian Grigg, and others suggest
// computing authenticity using derived key and HmacSHA1 of IV + ciphertext.
// However they also argue that what should be returned and treated as
// (i.e., stored as) ciphertext would be something like this:
// len_of_raw_ciphertext + IV + raw_ciphertext + MAC
// However, Schneier's & Ferguson's Horton Principle would argue
// that whatever data that one sends needs to be authenticated, so
// that would minimally mean that len_of_raw_ciphertext would need
// to be included in the MAC calculation. Failure to heed the Horton
// Principle has already resulted in CVE-2013-5960.
//
// TODO: Need to do something like this for custom serialization and then
// document order / format so it can be used by other ESAPI implementations.
public void computeAndStoreMAC(SecretKey authKey) {
if ( macComputed() ) {
String exm = "Programming error: Can't store message authentication code " +
"while encrypting; computeAndStoreMAC() called multiple times.";
throw new EnterpriseSecurityRuntimeException(exm, exm);
}
if ( ! collectedAll() ) {
String exm = "Have not collected all required information to compute and store MAC.";
throw new EnterpriseSecurityRuntimeException(exm, exm);
}
byte[] result = computeMAC(authKey);
if ( result != null ) {
storeSeparateMAC(result);
}
// If 'result' is null, we already logged this in computeMAC().
}
/**
* Same as {@link #computeAndStoreMAC(SecretKey)} but this is only used by
* {@code CipherTextSerializeer}. (Has package level access.)
*/ // CHECKME: For this to be "safe", it requires ESAPI jar to be sealed.
void storeSeparateMAC(byte[] macValue) {
if ( !macComputed() ) {
separate_mac_ = new byte[ macValue.length ];
CryptoHelper.copyByteArray(macValue, separate_mac_);
// This assertion should be okay as it's just a sanity check.
assert macComputed() : "MAC failed to compute correctly!";
}
}
/**
* Validate the message authentication code (MAC) associated with the ciphertext.
* This is mostly meant to ensure that an attacker has not replaced the IV
* or raw ciphertext with something arbitrary. Note however that it will
* not detect the case where an attacker simply substitutes one
* valid ciphertext with another ciphertext.
*
* @param authKey The secret key that is used for proving authenticity of
* the IV and ciphertext. This key should be derived from
* the {@code SecretKey} passed to the
* {@link Encryptor#encrypt(javax.crypto.SecretKey, PlainText)}
* and
* {@link Encryptor#decrypt(javax.crypto.SecretKey, CipherText)}
* methods or the "master" key when those corresponding
* encrypt / decrypt methods are used. This authenticity key
* should be the same length and for the same cipher algorithm
* as this {@code SecretKey}. The method
* {@link org.owasp.esapi.crypto.CryptoHelper#computeDerivedKey(SecretKey, int, String)}
* is a secure way to produce this derived key.
* @return True if the ciphertext has not be tampered with, and false otherwise.
*/
public boolean validateMAC(SecretKey authKey) {
boolean requiresMAC = ESAPI.securityConfiguration().useMACforCipherText();
if ( requiresMAC && macComputed() ) { // Uses MAC and it was computed
// Calculate MAC from HMAC-SHA1(nonce, IV + plaintext) and
// compare to stored value (separate_mac_). If same, then return true,
// else return false.
byte[] mac = computeMAC(authKey);
if ( mac.length != separate_mac_.length ) {
// Note: We want some type of unchecked exception
// here so this will not require code changes.
// Unfortunately, EncryptionException, which might
// make more sense here, is not a RuntimeException.
String exm = "MACs are of different lengths. " +
"Should both be the same length";
throw new EnterpriseSecurityRuntimeException(exm,
"Possible tampering of MAC? " + exm +
"computed MAC len: " + mac.length +
", received MAC len: " + separate_mac_.length);
}
return java.security.MessageDigest.isEqual(mac, separate_mac_); // Safe compare in JDK 7 and later
} else if ( ! requiresMAC ) { // Doesn't require a MAC
return true;
} else {
// This *used* to be the case (for versions 2.0 and 2.0.1) where we tried to
// accomodate the deprecated decrypt() method from ESAPI 1.4. Unfortunately,
// that was an EPIC FAIL. (See Google Issue # 306 for details.)
logger.warning(Logger.SECURITY_FAILURE, "MAC may have been tampered with (e.g., length set to 0).");
return false; // Deprecated decrypt() method removed, so now return false.
}
}
/**
* Return this {@code CipherText} object as a portable (i.e., network byte
* ordered) serialized byte array. Note this is not the same as
* returning a serialized object using Java serialization. Instead this
* is a representation that all ESAPI implementations will use to pass
* ciphertext between different programming language implementations.
*
* @return A network byte-ordered serialized representation of this object.
* @throws EncryptionException
*/ // DISCUSS: This method name sucks too. Suggestions???
public byte[] asPortableSerializedByteArray() throws EncryptionException {
// Check if this CipherText object is "complete", i.e., all
// mandatory has been collected.
if ( ! collectedAll() ) {
String msg = "Can't serialize this CipherText object yet as not " +
"all mandatory information has been collected";
throw new EncryptionException("Can't serialize incomplete ciphertext info", msg);
}
// If we are supposed to be using a (separate) MAC, also make sure
// that it has been computed/stored.
boolean requiresMAC = ESAPI.securityConfiguration().useMACforCipherText();
if ( requiresMAC && ! macComputed() ) {
String msg = "Programming error: MAC is required for this cipher mode (" +
getCipherMode() + "), but MAC has not yet been " +
"computed and stored. Call the method " +
"computeAndStoreMAC(SecretKey) first before " +
"attempting serialization.";
throw new EncryptionException("Can't serialize ciphertext info: Data integrity issue.",
msg);
}
// OK, everything ready, so give it a shot.
return new CipherTextSerializer(this).asSerializedByteArray();
}
///// Setters /////
/**
* Set the raw ciphertext.
* @param ciphertext The raw ciphertext.
* @throws EncryptionException Thrown if the MAC has already been computed
* via {@link #computeAndStoreMAC(SecretKey)}.
*/
public void setCiphertext(byte[] ciphertext)
throws EncryptionException
{
if ( ! macComputed() ) {
if ( ciphertext == null || ciphertext.length == 0 ) {
throw new EncryptionException("Encryption faled; no ciphertext",
"Ciphertext may not be null or 0 length!");
}
if ( isCollected(CipherTextFlags.CIPHERTEXT) ) {
logger.warning(Logger.SECURITY_FAILURE, "Raw ciphertext was already set; resetting.");
}
raw_ciphertext_ = new byte[ ciphertext.length ];
CryptoHelper.copyByteArray(ciphertext, raw_ciphertext_);
received(CipherTextFlags.CIPHERTEXT);
setEncryptionTimestamp();
} else {
String logMsg = "Programming error: Attempt to set ciphertext after MAC already computed.";
logger.error(Logger.SECURITY_FAILURE, logMsg);
throw new EncryptionException("MAC already set; cannot store new raw ciphertext", logMsg);
}
}
/**
* Set the IV and raw ciphertext.
* @param iv The initialization vector.
* @param ciphertext The raw ciphertext.
* @throws EncryptionException
*/
public void setIVandCiphertext(byte[] iv, byte[] ciphertext)
throws EncryptionException
{
if ( isCollected(CipherTextFlags.INITVECTOR) ) {
logger.warning(Logger.SECURITY_FAILURE, "IV was already set; resetting.");
}
if ( isCollected(CipherTextFlags.CIPHERTEXT) ) {
logger.warning(Logger.SECURITY_FAILURE, "Raw ciphertext was already set; resetting.");
}
if ( ! macComputed() ) {
if ( ciphertext == null || ciphertext.length == 0 ) {
throw new EncryptionException("Encryption faled; no ciphertext",
"Ciphertext may not be null or 0 length!");
}
if ( iv == null || iv.length == 0 ) {
if ( requiresIV() ) {
throw new EncryptionException("Encryption failed -- mandatory IV missing", // DISCUSS - also log? See below.
"Cipher mode " + getCipherMode() + " has null or empty IV");
}
} else if ( iv.length != getBlockSize() ) {
// TODO: FIXME: As per email from Jeff Walton to Kevin Wall dated 12/03/2013,
// this is not always true. E.g., for CCM, the IV length is supposed
// to be 7, 8, 7, 8, 9, 10, 11, 12, or 13 octets because of
// it's formatting function, the restof the octets used by the
// nonce/counter.
throw new EncryptionException("Encryption failed -- bad parameters passed to encrypt", // DISCUSS - also log? See below.
"IV length does not match cipher block size of " + getBlockSize());
}
cipherSpec_.setIV(iv);
received(CipherTextFlags.INITVECTOR);
setCiphertext( ciphertext );
} else {
String logMsg = "MAC already computed from previously set IV and raw ciphertext; may not be reset -- object is immutable.";
logger.error(Logger.SECURITY_FAILURE, logMsg); // Discuss: By throwing, this gets logged as warning, but it's really error! Why is an exception only a warning???
throw new EncryptionException("Validation of decryption failed.", logMsg);
}
}
public int getKDFVersion() {
return kdfVersion_;
}
public void setKDFVersion(int vers) {
CryptoHelper.isValidKDFVersion(vers, false, true);
kdfVersion_ = vers;
}
public KeyDerivationFunction.PRF_ALGORITHMS getKDF_PRF() {
return KeyDerivationFunction.convertIntToPRF(kdfPrfSelection_);
}
int kdfPRFAsInt() {
return kdfPrfSelection_;
}
public void setKDF_PRF(int prfSelection) {
if ( prfSelection < 0 || prfSelection > 15 ) {
throw new IllegalArgumentException("kdfPrf == " + prfSelection + " must be between 0 and 15, inclusive.");
}
kdfPrfSelection_ = prfSelection;
}
/** Get stored time stamp representing when data was encrypted. */
public long getEncryptionTimestamp() {
return encryption_timestamp_;
}
/**
* Set the encryption timestamp to the current system time as determined by
* {@code System.currentTimeMillis()}, but only if it has not been previously
* set. That is, this method ony has an effect the first time that it is
* called for this object.
*/
private void setEncryptionTimestamp() {
// We want to skip this when it's already been set via the package
// level call setEncryptionTimestamp(long) done via CipherTextSerializer
// otherwise it gets reset to the current time. But when it's restored
// from a serialized CipherText object, we want to keep the original
// encryption timestamp.
if ( encryption_timestamp_ != 0 ) {
logger.warning(Logger.EVENT_FAILURE, "Attempt to reset non-zero " +
"CipherText encryption timestamp to current time!");
}
encryption_timestamp_ = System.currentTimeMillis();
}
/**
* Set the encryption timestamp to the time stamp specified by the parameter.
*
* This method is intended for use only by {@code CipherTextSerializer}.
*
* @param timestamp The time in milliseconds since epoch time (midnight,
* January 1, 1970 GMT).
*/ // Package level access. ESAPI jar should be sealed and signed.
void setEncryptionTimestamp(long timestamp) {
if ( timestamp <= 0 ) {
throw new IllegalArgumentException("Timestamp must be greater than zero.");
}
if ( encryption_timestamp_ == 0 ) { // Only set it if it's not yet been set.
logger.warning(Logger.EVENT_FAILURE, "Attempt to reset non-zero " +
"CipherText encryption timestamp to " + new Date(timestamp) + "!");
}
encryption_timestamp_ = timestamp;
}
/** Return the separately calculated Message Authentication Code (MAC) that
* is computed via the {@code computeAndStoreMAC(SecretKey authKey)} method.
* @return The copy of the computed MAC, or {@code null} if one is not used.
*/
public byte[] getSeparateMAC() {
if ( separate_mac_ == null ) {
return null;
}
byte[] copy = new byte[ separate_mac_.length ];
System.arraycopy(separate_mac_, 0, copy, 0, separate_mac_.length);
return copy;
}
/**
* More useful {@code toString()} method.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder( "CipherText: " );
String creationTime = (( getEncryptionTimestamp() == 0) ? "No timestamp available" :
(new Date(getEncryptionTimestamp())).toString());
int n = getRawCipherTextByteLength();
String rawCipherText = (( n > 0 ) ? "present (" + n + " bytes)" : "absent");
String mac = (( separate_mac_ != null ) ? "present" : "absent");
sb.append("KDF Version: ").append( kdfVersion_ );
sb.append(", KDF PRF: ").append( kdfPRFAsInt() );
sb.append("; Creation time: ").append(creationTime);
sb.append("; raw ciphertext is ").append(rawCipherText);
sb.append("; MAC is ").append(mac).append("; ");
sb.append( cipherSpec_.toString() );
return sb.toString();
}
/**
* {@inheritDoc}
*/
@Override public boolean equals(Object other) {
boolean result = false;
if ( this == other )
return true;
if ( other == null )
return false;
if ( other instanceof CipherText) {
CipherText that = (CipherText)other;
if ( this.collectedAll() && that.collectedAll() ) {
result = (that.canEqual(this) &&
this.cipherSpec_.equals(that.cipherSpec_) &&
// Safe comparison, resistant to timing attacks
java.security.MessageDigest.isEqual(this.raw_ciphertext_, that.raw_ciphertext_) &&
java.security.MessageDigest.isEqual(this.separate_mac_, that.separate_mac_) &&
this.encryption_timestamp_ == that.encryption_timestamp_ );
} else {
logger.warning(Logger.EVENT_FAILURE, "CipherText.equals(): Cannot compare two " +
"CipherText objects that are not complete, and therefore immutable!");
logger.info(Logger.EVENT_FAILURE, "This CipherText: " + this.collectedAll() + ";" +
"other CipherText: " + that.collectedAll());
logger.info(Logger.EVENT_FAILURE, "CipherText.equals(): Progress comparison: " +
((this.progress == that.progress) ? "Same" : "Different"));
logger.info(Logger.EVENT_FAILURE, "CipherText.equals(): Status this: " + this.progress +
"; status other CipherText object: " + that.progress);
// CHECKME: Perhaps we should throw a RuntimeException instead???
return false;
}
}
return result;
}
/**
* {@inheritDoc}
*/
@Override public int hashCode() {
if ( this.collectedAll() ) {
logger.warning(Logger.EVENT_FAILURE, "CipherText.hashCode(): Cannot compute " +
"hachCode() of incomplete CipherText object; object not immutable- " +
"returning 0.");
// CHECKME: Throw RuntimeException instead?
return 0;
}
StringBuilder sb = new StringBuilder();
sb.append( cipherSpec_.hashCode() );
sb.append( encryption_timestamp_ );
String raw_ct = null;
String mac = null;
try {
raw_ct = new String(raw_ciphertext_, "UTF-8");
// Remember, MAC is optional even when CipherText is complete.
mac = new String( ((separate_mac_ != null) ? separate_mac_ : new byte[] { }), "UTF-8");
} catch(UnsupportedEncodingException ex) {
// Should never happen as UTF-8 encode supported by rt.jar,
// but it it does, just use default encoding.
raw_ct = new String(raw_ciphertext_);
mac = new String( ((separate_mac_ != null) ? separate_mac_ : new byte[] { }));
}
sb.append( raw_ct );
sb.append( mac );
return sb.toString().hashCode();
}
/**
* Needed for correct definition of equals for general classes.
* (Technically not needed for 'final' classes though like this class
* though; this will just allow it to work in the future should we
* decide to allow * sub-classing of this class.)
*
* See
* @link http://www.artima.com/lejava/articles/equality.html
* for full explanation.
*
*/
protected boolean canEqual(Object other) {
return (other instanceof CipherText);
}
//////////////////////////////////// P R I V A T E /////////////////////////////////////////
/**
* Compute a MAC, but do not store it. May set the nonce value as a
* side-effect. The MAC is calculated as:
*
* HMAC-SHA1(nonce, IV + plaintext)
*
* Note that only HMAC-SHA1 is used for the MAC calcuation. Unlike
* the PRF used for derived key generation in the {@code KeyDerivationFunction}
* class, the user cannot change the algorithm used to compute the MAC itself.
* One reason for that is that we don't want the MAC value to be excessively
* long; 128 bits is already quite long when only encrypting short strings.
* Also while the NSA reviewed this and were okay with it, Bellare, Canetti & Krawczyk
* proved in 1996 [see http://pssic.free.fr/Extra%20Reading/SEC+/SEC+/hmac-cb.pdf] that
* HMAC security doesn’t require that the underlying hash function be collision resistant,
* but only that it acts as a pseudo-random function, which SHA1 satisfies.
* @param authKey The {@Code SecretKey} used with the computed HMAC-SHA1
* to ensure authenticity.
* @return The value for the MAC.
*/
private byte[] computeMAC(SecretKey authKey) {
// These assertions are okay and leaving them as assertions rather than
// changing the to conditional statements that throw should be all right
// because this is private method and presumably we should have already
// checked things in the public or protected methods where appropriate.
if ( raw_ciphertext_ == null || raw_ciphertext_.length == 0 ) {
String exm = "Raw ciphertext may not be null or empty.";
throw new EnterpriseSecurityRuntimeException(exm, exm);
}
if ( authKey == null || authKey.getEncoded().length == 0 ) {
String exm = "Authenticity secret key may not be null or zero length.";
throw new EnterpriseSecurityRuntimeException(exm, exm);
}
try {
// IMPORTANT NOTE: The NSA review was (apparently) OK with using HmacSHA1
// to calculate the MAC that ensures authenticity of the IV+ciphertext.
// (Not true of calculation of the use HmacSHA1 for the KDF though.) Therefore,
// we did not make this configurable. Note also that choosing an improved
// MAC algorithm here would cause the overall length of the serialized ciphertext
// to be just that much longer, which is probably unacceptable when encrypting
// short strings.
SecretKey sk = new SecretKeySpec(authKey.getEncoded(), "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(sk);
if ( requiresIV() ) {
mac.update( getIV() );
}
byte[] result = mac.doFinal( getRawCipherText() );
return result;
} catch (NoSuchAlgorithmException e) {
logger.error(Logger.SECURITY_FAILURE, "Cannot compute MAC w/out HmacSHA1.", e);
return null;
} catch (InvalidKeyException e) {
logger.error(Logger.SECURITY_FAILURE, "Cannot comput MAC; invalid 'key' for HmacSHA1.", e);
return null;
}
}
/**
* Return true if the MAC has already been computed (i.e., not null).
*/
private boolean macComputed() {
return (separate_mac_ != null);
}
/**
* Return true if we've collected all the required pieces; otherwise false.
*/
private boolean collectedAll() {
EnumSet ctFlags = null;
if ( requiresIV() ) {
ctFlags = allCtFlags;
} else {
EnumSet initVector = EnumSet.of(CipherTextFlags.INITVECTOR);
ctFlags = EnumSet.complementOf(initVector);
}
boolean result = progress.containsAll(ctFlags);
return result;
}
/** Check if we've collected a specific flag type.
* @param flag The flag type; e.g., {@code CipherTextFlags.INITVECTOR}, etc.
* @return Return true if we've collected a specific flag type; otherwise false.
*/
private boolean isCollected(CipherTextFlags flag) {
return progress.contains(flag);
}
/**
* Add the flag to the set of what we've already collected.
* @param flag The flag type to be added; e.g., {@code CipherTextFlags.INITVECTOR}.
*/
private void received(CipherTextFlags flag) {
progress.add(flag);
}
/**
* Add all the flags from the specified set to that we've collected so far.
* @param ctSet A {@code EnumSet} containing all the flags
* we wish to add.
*/
private void received(EnumSet ctSet) {
Iterator it = ctSet.iterator();
while ( it.hasNext() ) {
received( it.next() );
}
}
/**
* Based on the KDF version and the selected MAC algorithm for the KDF PRF,
* calculate the 32-bit quantity representing these.
* @return A 4-byte (octet) quantity representing the KDF version and the
* MAC algorithm used for the KDF's Pseudo-Random Function.
* @see Format of portable serialization of org.owasp.esapi.crypto.CipherText object (pg 2)
*/
public int getKDFInfo() {
final int unusedBit28 = 0x8000000; // 1000000000000000000000000000
// kdf version is bits 1-27, bit 28 (reserved) should be 0, and
// bits 29-32 are the MAC algorithm indicating which PRF to use for the KDF.
int kdfVers = this.getKDFVersion();
if ( ! CryptoHelper.isValidKDFVersion(kdfVers, true, false) ) {
String exm = "Invalid KDF version encountered. Value as" + kdfVers;
throw new EnterpriseSecurityRuntimeException(exm,
"Possible tampering of KDF version #? " + exm);
}
int kdfInfo = kdfVers;
int macAlg = kdfPRFAsInt();
if ( macAlg < 0 || macAlg > 15 ) {
String exm = "Invalid specifier for MAC algorithm: " + macAlg;
throw new EnterpriseSecurityRuntimeException(exm,
"Possible tampering of macAlg specifier? " + exm +
"; value should be 0 <= macAlg <= 15.");
}
// Make sure bit28 is cleared. (Reserved for future use.)
kdfInfo &= ~unusedBit28;
// Set MAC algorithm bits in high (MSB) nibble.
kdfInfo |= (macAlg << 28);
return kdfInfo;
}
}