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

org.owasp.esapi.crypto.CipherText Maven / Gradle / Ivy

/*
 * 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy