org.owasp.esapi.crypto.CryptoHelper 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 (c) 2009 - The OWASP Foundation
*/
package org.owasp.esapi.crypto;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.InvalidParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Logger;
import org.owasp.esapi.errors.EncryptionException;
/**
* Class to provide some convenience methods for encryption, decryption, etc.
*
* All the cryptographic operations use the default cryptographic properties;
* e.g., default cipher transformation, default key size, default IV type (where
* applicable), etc.
*
* @author [email protected]
* @since 2.0
*/
public class CryptoHelper {
private static final Logger logger = ESAPI.getLogger("CryptoHelper");
// TODO: Also consider supplying implementation of RFC 2898 / PKCS#5 PBKDF2
// in this file as well??? Maybe save for ESAPI 2.1 or 3.0.
/**
* Generate a random secret key appropriate to the specified cipher algorithm
* and key size.
* @param alg The cipher algorithm or cipher transformation. (If the latter is
* passed, the cipher algorithm is determined from it.) Cannot be
* null or empty.
* @param keySize The key size, in bits.
* @return A random {@code SecretKey} is returned.
* @throws EncryptionException Thrown if cannot create secret key conforming to
* requested algorithm with requested size. Typically this is caused by
* specifying an unavailable algorithm or invalid key size.
*/
public static SecretKey generateSecretKey(String alg, int keySize)
throws EncryptionException
{
assert alg != null : "Algorithm must not be null."; // NPE if null and assertions disabled.
assert !alg.equals("") : "Algorithm must not be empty"; // NoSuchAlgorithmExeption if empty & assertions disabled.
assert keySize > 0 : "Key size must be positive."; // Usually should be even multiple of 8, but not strictly required by alg.
// Don't use CipherSpec here to get algorithm as this may cause assertion
// to fail (when enabled) if only algorithm name is passed to us.
String[] cipherSpec = alg.split("/");
String cipherAlg = cipherSpec[0];
try {
// Special case for things like PBEWithMD5AndDES or PBEWithSHA1AndDESede.
// In such cases, the key generator should only request an instance of "PBE".
if ( cipherAlg.toUpperCase().startsWith("PBEWITH") ) {
cipherAlg = "PBE";
}
KeyGenerator kgen =
KeyGenerator.getInstance( cipherAlg );
kgen.init(keySize);
return kgen.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new EncryptionException("Failed to generate random secret key",
"Invalid algorithm. Failed to generate secret key for " + alg + " with size of " + keySize + " bits.", e);
} catch (InvalidParameterException e) {
throw new EncryptionException("Failed to generate random secret key - invalid key size specified.",
"Invalid key size. Failed to generate secret key for " + alg + " with size of " + keySize + " bits.", e);
}
}
/**
* The method is ESAPI's Key Derivation Function (KDF) that computes a
* derived key from the {@code keyDerivationKey} for either
* encryption / decryption or for authentication.
*
* CAUTION: If this algorithm for computing derived keys from the
* key derivation key is ever changed, we risk breaking backward compatibility of being
* able to decrypt data previously encrypted with earlier / different versions
* of this method. Therefore, do not change this unless you are 100% certain that
* what you are doing will NOT change either of the derived keys for
* ANY "key derivation key" AT ALL!!!
*
* NOTE: This method is generally not intended to be called separately.
* It is used by ESAPI's reference crypto implementation class {@code JavaEncryptor}
* and might be useful for someone implementing their own replacement class, but
* generally it is not something that is useful to application client code.
*
* @param keyDerivationKey A key used as an input to a key derivation function
* to derive other keys. This is the key that generally
* is created using some key generation mechanism such as
* {@link #generateSecretKey(String, int)}. The
* "input" key from which the other keys are derived.
* The derived key will have the same algorithm type
* as this key.
* @param keySize The cipher's key size (in bits) for the {@code keyDerivationKey}.
* Must have a minimum size of 56 bits and be an integral multiple of 8-bits.
* Note: The derived key will have the same size as this.
* @param purpose The purpose for the derived key. Must be either the
* string "encryption" or "authenticity". Use "encryption" for
* creating a derived key to use for confidentiality, and "authenticity"
* for a derived key to use with a MAC to ensure message authenticity.
* @return The derived {@code SecretKey} to be used according
* to the specified purpose. Note that this serves the same purpose
* as "label" in section 5.1 of NIST SP 800-108.
* @throws NoSuchAlgorithmException The {@code keyDerivationKey} has an unsupported
* encryption algorithm or no current JCE provider supports
* "HmacSHA1".
* @throws EncryptionException If "UTF-8" is not supported as an encoding, then
* this is thrown with the original {@code UnsupportedEncodingException}
* as the cause. (NOTE: This should never happen as "UTF-8" is supposed to
* be a common encoding supported by all Java implementations. Support
* for it is usually in rt.jar.)
* @throws InvalidKeyException Likely indicates a coding error. Should not happen.
* @throws EncryptionException Throw for some precondition violations.
* @deprecated Use{@code KeyDerivationFunction} instead. This method will be removed as of
* ESAPI release 2.1 so if you are using this, please change your code.
*/
public static SecretKey computeDerivedKey(SecretKey keyDerivationKey, int keySize, String purpose)
throws NoSuchAlgorithmException, InvalidKeyException, EncryptionException
{
// These really should be turned into actual runtime checks and an
// IllegalArgumentException should be thrown if they are violated.
assert keyDerivationKey != null : "Key derivation key cannot be null.";
// We would choose a larger minimum key size, but we want to be
// able to accept DES for legacy encryption needs.
assert keySize >= 56 : "Key has size of " + keySize + ", which is less than minimum of 56-bits.";
assert (keySize % 8) == 0 : "Key size (" + keySize + ") must be a even multiple of 8-bits.";
assert purpose != null;
assert purpose.equals("encryption") || purpose.equals("authenticity") :
"Purpose must be \"encryption\" or \"authenticity\".";
// DISCUSS: Should we use HmacSHA1 (what we were using) or the HMAC defined by
// Encryptor.KDF.PRF instead? Either way, this is not compatible with
// previous ESAPI versions. JavaEncryptor doesn't use this any longer.
KeyDerivationFunction kdf = new KeyDerivationFunction(
KeyDerivationFunction.PRF_ALGORITHMS.HmacSHA1);
return kdf.computeDerivedKey(keyDerivationKey, keySize, purpose);
}
/**
* Return true if specified cipher mode is one of those specified in the
* {@code ESAPI.properties} file that supports both confidentiality
* and authenticity (i.e., a "combined cipher mode" as NIST refers
* to it).
* @param cipherMode The specified cipher mode to be used for the encryption
* or decryption operation.
* @return true if the specified cipher mode is in the comma-separated list
* of cipher modes supporting both confidentiality and authenticity;
* otherwise false.
* @see org.owasp.esapi.SecurityConfiguration#getCombinedCipherModes()
*/
public static boolean isCombinedCipherMode(String cipherMode)
{
assert cipherMode != null : "Cipher mode may not be null";
assert ! cipherMode.equals("") : "Cipher mode may not be empty string";
List combinedCipherModes =
ESAPI.securityConfiguration().getCombinedCipherModes();
return combinedCipherModes.contains( cipherMode );
}
/**
* Return true if specified cipher mode is one that may be used for
* encryption / decryption operations via {@link org.owasp.esapi.Encryptor}.
* @param cipherMode The specified cipher mode to be used for the encryption
* or decryption operation.
* @return true if the specified cipher mode is in the comma-separated list
* of cipher modes supporting both confidentiality and authenticity;
* otherwise false.
* @see #isCombinedCipherMode(String)
* @see org.owasp.esapi.SecurityConfiguration#getCombinedCipherModes()
* @see org.owasp.esapi.SecurityConfiguration#getAdditionalAllowedCipherModes()
*/
public static boolean isAllowedCipherMode(String cipherMode)
{
if ( isCombinedCipherMode(cipherMode) ) {
return true;
}
List extraCipherModes =
ESAPI.securityConfiguration().getAdditionalAllowedCipherModes();
return extraCipherModes.contains( cipherMode );
}
/**
* Check to see if a Message Authentication Code (MAC) is required
* for a given {@code CipherText} object and the current ESAPI.property
* settings. A MAC is considered "required" if the specified
* {@code CipherText} was not encrypted by one of the preferred
* "combined" cipher modes (e.g., CCM or GCM) and the setting of the
* current ESAPI properties for the property
* {@code Encryptor.CipherText.useMAC} is set to {@code true}. (Normally,
* the setting for {@code Encryptor.CipherText.useMAC} should be set to
* {@code true} unless FIPS 140-2 compliance is required. See
*
* User Guide for Symmetric Encryption in ESAPI 2.0 and the section
* on using ESAPI with FIPS for further details.
*
* @param ct The specified {@code CipherText} object to check to see if
* it requires a MAC.
* @returns True if a MAC is required, false if it is not required.
*/
public static boolean isMACRequired(CipherText ct) {
boolean preferredCipherMode =
CryptoHelper.isCombinedCipherMode( ct.getCipherMode() );
boolean wantsMAC = ESAPI.securityConfiguration().useMACforCipherText();
// The preferred "combined" cipher modes such as CCM, GCM, etc. do
// not require a MAC as a MAC would be superfluous and just require
// additional computing time.
return ( !preferredCipherMode && wantsMAC );
}
/**
* If a Message Authentication Code (MAC) is required for the specified
* {@code CipherText} object, then attempt to validate the MAC that
* should be embedded within the {@code CipherText} object by using a
* derived key based on the specified {@code SecretKey}.
*
* @param sk The {@code SecretKey} used to derived a key to check
* the authenticity via the MAC.
* @param ct The {@code CipherText} that we are checking for a
* valid MAC.
*
* @return True is returned if a MAC is required and it is valid as
* verified using a key derived from the specified
* {@code SecretKey} or a MAC is not required. False is returned
* otherwise.
*/
public static boolean isCipherTextMACvalid(SecretKey sk, CipherText ct)
{
if ( CryptoHelper.isMACRequired( ct ) ) {
try {
SecretKey authKey = CryptoHelper.computeDerivedKey( sk, ct.getKeySize(), "authenticity");
boolean validMAC = ct.validateMAC( authKey );
return validMAC;
} catch (Exception ex) {
// Error on side of security. If this fails and can't verify MAC
// assume it is invalid. Note that CipherText.toString() does not
// print the actual ciphertext.
logger.warning(Logger.SECURITY_FAILURE, "Unable to validate MAC for ciphertext " + ct, ex);
return false;
}
}
return true;
}
/**
* Overwrite a byte array with a specified byte. This is frequently done
* to a plaintext byte array so the sensitive data is not lying around
* exposed in memory.
* @param bytes The byte array to be overwritten.
* @param x The byte array {@code bytes} is overwritten with this byte.
*/
public static void overwrite(byte[] bytes, byte x)
{
Arrays.fill(bytes, x);
}
/**
* Overwrite a byte array with the byte containing '*'. That is, call
*
* overwrite(bytes, (byte)'*');
*
* @param bytes The byte array to be overwritten.
*/
public static void overwrite(byte[] bytes)
{
overwrite(bytes, (byte)'*');
}
// These provide for a bit more type safety when copying bytes around.
/**
* Same as {@code System.arraycopy(src, 0, dest, 0, length)}.
*
* @param src the source array.
* @param dest the destination array.
* @param length the number of array elements to be copied.
* @exception IndexOutOfBoundsException if copying would cause
* access of data outside array bounds.
* @exception NullPointerException if either src
or
* dest
is null
.
*/
public static void copyByteArray(final byte[] src, byte[] dest, int length)
{
System.arraycopy(src, 0, dest, 0, length);
}
/**
* Same as {@code copyByteArray(src, dest, src.length)}.
* @param src the source array.
* @param dest the destination array.
* @exception IndexOutOfBoundsException if copying would cause
* access of data outside array bounds.
* @exception NullPointerException if either src
or
* dest
is null
.
*/
public static void copyByteArray(final byte[] src, byte[] dest)
{
copyByteArray(src, dest, src.length);
}
/**
* A "safe" array comparison that is not vulnerable to side-channel
* "timing attacks". All comparisons of non-null, equal length bytes should
* take same amount of time. We use this for cryptographic comparisons.
*
* @param b1 A byte array to compare.
* @param b2 A second byte array to compare.
* @return {@code true} if both byte arrays are null or if both byte
* arrays are identical or have the same value; otherwise
* {@code false} is returned.
*/
public static boolean arrayCompare(byte[] b1, byte[] b2) {
if ( b1 == b2 ) {
return true;
}
if ( b1 == null || b2 == null ) {
return (b1 == b2);
}
if ( b1.length != b2.length ) {
return false;
}
int result = 0;
// Make sure to go through ALL the bytes. We use the fact that if
// you XOR any bit stream with itself the result will be all 0 bits,
// which in turn yields 0 for the result.
for(int i = 0; i < b1.length; i++) {
// XOR the 2 current bytes and then OR with the outstanding result.
result |= (b1[i] ^ b2[i]);
}
return (result == 0) ? true : false;
}
/**
* Prevent public, no-argument CTOR from being auto-generated. Public CTOR
* is not needed as all methods are static.
*/
private CryptoHelper() {
}
}