Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.owasp.esapi.reference.crypto.JavaEncryptor 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 (c) 2007 - The OWASP Foundation
*
* The ESAPI is published by OWASP under the BSD license. You should read and accept the
* LICENSE before you use, modify, and/or redistribute this software.
*
* @author Jeff Williams Aspect Security
* @author [email protected]
* @created 2007
*/
package org.owasp.esapi.reference.crypto;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
// import javax.crypto.Mac; // Uncomment if computeHMAC() is included.
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.EncoderConstants;
import org.owasp.esapi.Encryptor;
import org.owasp.esapi.Logger;
import org.owasp.esapi.codecs.Hex;
import org.owasp.esapi.crypto.CipherSpec;
import org.owasp.esapi.crypto.CipherText;
import org.owasp.esapi.crypto.CryptoHelper;
import org.owasp.esapi.crypto.KeyDerivationFunction;
import org.owasp.esapi.crypto.PlainText;
import org.owasp.esapi.crypto.SecurityProviderLoader;
import org.owasp.esapi.errors.ConfigurationException;
import org.owasp.esapi.errors.EncryptionException;
import org.owasp.esapi.errors.IntegrityException;
import org.owasp.esapi.reference.DefaultSecurityConfiguration;
/**
* Reference implementation of the {@code Encryptor} interface. This implementation
* layers on the JCE provided cryptographic package. Algorithms used are
* configurable in the {@code ESAPI.properties} file. The main property
* controlling the selection of this class is {@code ESAPI.Encryptor}. Most of
* the other encryption related properties have property names that start with
* the string "Encryptor.".
*
* @author Jeff Williams (jeff.williams .at. aspectsecurity.com) Aspect Security
* @author [email protected]
* @author Chris Schmidt (chrisisbeef .at. gmail.com)
* @since June 1, 2007; some methods since ESAPI Java 2.0
* @see org.owasp.esapi.Encryptor
*/
public final class JavaEncryptor implements Encryptor {
private static volatile Encryptor singletonInstance;
// Note: This double-check pattern only works because singletonInstance
// is declared to be volatile. Usually this method is called
// via ESAPI.encryptor() rather than directly.
public static Encryptor getInstance() throws EncryptionException {
if ( singletonInstance == null ) {
synchronized ( JavaEncryptor.class ) {
if ( singletonInstance == null ) {
singletonInstance = new JavaEncryptor();
}
}
}
return singletonInstance;
}
private static boolean initialized = false;
// encryption
private static SecretKeySpec secretKeySpec = null; // DISCUSS: Why static? Implies one key?!?
private static String encryptAlgorithm = "AES";
private static String encoding = "UTF-8";
private static int encryptionKeyLength = 128;
// digital signatures
private static PrivateKey privateKey = null;
private static PublicKey publicKey = null;
private static String signatureAlgorithm = "SHA1withDSA";
private static String randomAlgorithm = "SHA1PRNG";
private static int signatureKeyLength = 1024;
// hashing
private static String hashAlgorithm = "SHA-512";
private static int hashIterations = 1024;
// Logging - DISCUSS: This "sticks" us with a specific logger to whatever it was when
// this class is first loaded. Is this a big limitation? Since there
// is no method to reset it, we may has well make it 'final' also.
private static Logger logger = ESAPI.getLogger("JavaEncryptor");
// Used to print out warnings about deprecated methods.
private static int encryptCounter = 0;
private static int decryptCounter = 0;
// DISCUSS: OK to not have a property for this to set the frequency?
// The desire is to persuade people to move away from these
// two deprecated encrypt(String) / decrypt(String) methods,
// so perhaps the annoyance factor of not being able to
// change it will help. For now, it is just hard-coded here.
// We could be mean and just print a warning *every* time.
private static final int logEveryNthUse = 25;
// *Only* use this string for user messages for EncryptionException when
// decryption fails. This is to prevent information leakage that may be
// valuable in various forms of ciphertext attacks, such as the
// Padded Oracle attack described by Rizzo and Duong.
private static final String DECRYPTION_FAILED = "Decryption failed; see logs for details.";
// # of seconds that all failed decryption attempts will take. Used to
// help prevent side-channel timing attacks.
private static int N_SECS = 2;
// Load the preferred JCE provider if one has been specified.
static {
try {
SecurityProviderLoader.loadESAPIPreferredJCEProvider();
} catch (NoSuchProviderException ex) {
// Note that audit logging is done elsewhere in called method.
logger.fatal(Logger.SECURITY_FAILURE,
"JavaEncryptor failed to load preferred JCE provider.", ex);
throw new ExceptionInInitializerError(ex);
}
setupAlgorithms();
}
/**
* Generates a new strongly random secret key and salt that can be
* copy and pasted in the ESAPI.properties file.
*
* @param args Set first argument to "-print" to display available algorithms on standard output.
* @throws java.lang.Exception To cover a multitude of sins, mostly in configuring ESAPI.properties.
*/
public static void main( String[] args ) throws Exception {
System.out.println( "Generating a new secret master key" );
// print out available ciphers
if ( args.length == 1 && args[0].equalsIgnoreCase("-print" ) ) {
System.out.println( "AVAILABLE ALGORITHMS" );
Provider[] providers = Security.getProviders();
TreeMap tm = new TreeMap();
// DISCUSS: Note: We go through multiple providers, yet nowhere do I
// see where we print out the PROVIDER NAME. Not all providers
// will implement the same algorithms and some "partner" with
// whom we are exchanging different cryptographic messages may
// have _different_ providers in their java.security file. So
// it would be useful to know the provider name where each
// algorithm is implemented. Might be good to prepend the provider
// name to the 'key' with something like "providerName: ". Thoughts?
for (int i = 0; i != providers.length; i++) {
// DISCUSS: Print security provider name here???
// Note: For some odd reason, Provider.keySet() returns
// Set of the property keys (which are Strings)
// contained in this provider, but Set seems
// more appropriate. But that's why we need the cast below.
System.out.println("===== Provider " + i + ":" + providers[i].getName() + " ======");
Iterator it = providers[i].keySet().iterator();
while (it.hasNext()) {
String key = (String)it.next();
String value = providers[i].getProperty( key );
tm.put(key, value);
System.out.println("\t\t " + key + " -> "+ value );
}
}
Set< Entry > keyValueSet = tm.entrySet();
Iterator> it = keyValueSet.iterator();
while( it.hasNext() ) {
Map.Entry entry = it.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println( " " + key + " -> "+ value );
}
} else {
// Used to print a similar line to use '-print' even when it was specified.
System.out.println( "\tuse '-print' to also show available crypto algorithms from all the security providers" );
}
// setup algorithms -- Each of these have defaults if not set, although
// someone could set them to something invalid. If
// so a suitable exception will be thrown and displayed.
encryptAlgorithm = ESAPI.securityConfiguration().getEncryptionAlgorithm();
encryptionKeyLength = ESAPI.securityConfiguration().getEncryptionKeyLength();
randomAlgorithm = ESAPI.securityConfiguration().getRandomAlgorithm();
SecureRandom random = SecureRandom.getInstance(randomAlgorithm);
SecretKey secretKey = CryptoHelper.generateSecretKey(encryptAlgorithm, encryptionKeyLength);
byte[] raw = secretKey.getEncoded();
byte[] salt = new byte[20]; // Or 160-bits; big enough for SHA1, but not SHA-256 or SHA-512.
random.nextBytes( salt );
String eol = System.getProperty("line.separator", "\n"); // So it works on Windows too.
System.out.println( eol + "Copy and paste these lines into your ESAPI.properties" + eol);
System.out.println( "#==============================================================");
System.out.println( "Encryptor.MasterKey=" + ESAPI.encoder().encodeForBase64(raw, false) );
System.out.println( "Encryptor.MasterSalt=" + ESAPI.encoder().encodeForBase64(salt, false) );
System.out.println( "#==============================================================" + eol);
}
/**
* Private CTOR for {@code JavaEncryptor}, called by {@code getInstance()}.
* @throws EncryptionException if can't construct this object for some reason.
* Original exception will be attached as the 'cause'.
*/
private JavaEncryptor() throws EncryptionException {
byte[] salt = ESAPI.securityConfiguration().getMasterSalt();
byte[] skey = ESAPI.securityConfiguration().getMasterKey();
if ( salt == null ) {
throw new ConfigurationException("Can't obtain master salt, Encryptor.MasterSalt");
}
if ( salt.length < 16 ) {
throw new ConfigurationException("Encryptor.MasterSalt must be at least 16 bytes. " +
"Length is: " + salt.length + " bytes.");
}
if ( skey == null ) {
throw new ConfigurationException("Can't obtain master key, Encryptor.MasterKey");
}
if ( skey.length < 7 ) {
throw new ConfigurationException("Encryptor.MasterKey must be at least 7 bytes. " +
"Length is: " + skey.length + " bytes.");
}
// Set up secretKeySpec for use for symmetric encryption and decryption,
// and set up the public/private keys for asymmetric encryption /
// decryption.
// TODO: Note: Since we've dumped ESAPI 1.4 crypto backward compatibility,
// then we probably can ditch the Encryptor.EncryptionAlgorithm
// property. If so, encryptAlgorithm should probably use
// Encryptor.CipherTransformation and just pull off the cipher
// algorithm name so we can use it here. That just requires
// advance notice and proper deprecation, which I'm not prepared
// to do at this time. -kevin wall
synchronized(JavaEncryptor.class) {
if ( ! initialized ) {
//
// For symmetric encryption
//
// NOTE: FindBugs complains about this
// (ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD) but
// it should be OK since it is synchronized and only
// done once. While we could separate this out and
// handle in a static initializer, it just seems to
// fit better here.
secretKeySpec = new SecretKeySpec(skey, encryptAlgorithm );
//
// For asymmetric encryption (i.e., public/private key)
//
try {
SecureRandom prng = SecureRandom.getInstance(randomAlgorithm);
// Because hash() is not static (but it could be were in not
// for the interface method specification in Encryptor), we
// cannot do this initialization in a static method or static
// initializer.
byte[] seed = hash(new String(skey, encoding),new String(salt, encoding)).getBytes(encoding);
prng.setSeed(seed);
initKeyPair(prng);
} catch (Exception e) {
throw new EncryptionException("Encryption failure", "Error creating Encryptor", e);
}
// Mark everything as initialized.
initialized = true;
}
}
}
/**
* {@inheritDoc}
*
* Hashes the data with the supplied salt and the number of iterations specified in
* the ESAPI SecurityConfiguration.
*/
public String hash(String plaintext, String salt) throws EncryptionException {
return hash( plaintext, salt, hashIterations );
}
/**
* {@inheritDoc}
*
* Hashes the data using the specified algorithm and the Java MessageDigest class. This method
* first adds the salt, a separator (":"), and the data, and then rehashes the specified number of iterations
* in order to help strengthen weak passwords.
*/
public String hash(String plaintext, String salt, int iterations) throws EncryptionException {
byte[] bytes = null;
try {
MessageDigest digest = MessageDigest.getInstance(hashAlgorithm);
digest.reset();
digest.update(ESAPI.securityConfiguration().getMasterSalt());
digest.update(salt.getBytes(encoding));
digest.update(plaintext.getBytes(encoding));
// rehash a number of times to help strengthen weak passwords
bytes = digest.digest();
for (int i = 0; i < iterations; i++) {
digest.reset();
bytes = digest.digest(bytes);
}
String encoded = ESAPI.encoder().encodeForBase64(bytes,false);
return encoded;
} catch (NoSuchAlgorithmException e) {
throw new EncryptionException("Internal error", "Can't find hash algorithm " + hashAlgorithm, e);
} catch (UnsupportedEncodingException ex) {
throw new EncryptionException("Internal error", "Can't find encoding for " + encoding, ex);
}
}
/**
* {@inheritDoc}
*/
public CipherText encrypt(PlainText plaintext) throws EncryptionException {
// Now more of a convenience function for using the master key.
return encrypt(secretKeySpec, plaintext);
}
/**
* {@inheritDoc}
*/
public CipherText encrypt(SecretKey key, PlainText plain)
throws EncryptionException
{
if ( key == null ) {
throw new IllegalArgumentException("(Master) encryption key arg may not be null. Is Encryptor.MasterKey set?");
}
if ( plain == null ) {
throw new IllegalArgumentException("PlainText may arg not be null");
}
byte[] plaintext = plain.asBytes();
boolean overwritePlaintext = ESAPI.securityConfiguration().overwritePlainText();
boolean success = false; // Used in 'finally' clause.
String xform = null;
int keySize = key.getEncoded().length * 8; // Convert to # bits
try {
xform = ESAPI.securityConfiguration().getCipherTransformation();
String[] parts = xform.split("/");
if ( parts.length != 3 ) {
throw new ConfigurationException("Malformed cipher transformation: " + xform +
". Should have format of cipher_alg/cipher_mode/padding_scheme.");
}
String cipherMode = parts[1];
// This way we can prevent modes like OFB and CFB where the IV should never
// be repeated with the same encryption key (at least until we support
// Encryptor.ChooseIVMethod=specified and allow us to specify some mechanism
// to ensure the IV will never be repeated (such as a time stamp or other
// monotonically increasing function).
// DISCUSS: Should we include the permitted cipher modes in the exception msg?
if ( ! CryptoHelper.isAllowedCipherMode(cipherMode) ) {
throw new EncryptionException("Encryption failure: invalid cipher mode ( " + cipherMode + ") for encryption",
"Encryption failure: Cipher transformation " + xform + " specifies invalid " +
"cipher mode " + cipherMode);
}
// Note - Cipher is not thread-safe so we create one locally
// Also, we need to change this eventually so other algorithms can
// be supported. Eventually, there will be an encrypt() method that
// takes a (new class) CryptoControls, as something like this:
// public CipherText encrypt(CryptoControls ctrl, SecretKey skey, PlainText plaintext)
// and this method will just call that one.
Cipher encrypter = Cipher.getInstance(xform);
String cipherAlg = encrypter.getAlgorithm();
int minKeyLen = 112; // Use for hard-coded default to support 2TDEA
try {
minKeyLen = ESAPI.securityConfiguration().getIntProp("Encryptor.MinEncryptionKeyLength");
} catch( Exception ex ) {
logger.warning(Logger.EVENT_FAILURE,
"Property 'Encryptor.MinEncryptionKeyLength' not properly set in ESAPI.properties file; using hard coded default of 112 for min key size for encryption.",
ex);
; // Do NOT rethrow.
}
if ( keySize < minKeyLen ) {
// NOTE: This used to just log a warning. It now logs an error & throws an exception.
//
// ESAPI.EncryptionKeyLength defaults to 128. This means that someone wants to use ESAPI to
// encrypt with something like 2-key TDES, they would have to set this to that property
// to 112 bits.
logger.error(Logger.SECURITY_FAILURE, "Actual key size of " + keySize + " bits SMALLER THAN MINIMUM allowed " +
"encryption key length (ESAPI.EncryptionKeyLength) of " + minKeyLen + " bits with cipher algorithm " + cipherAlg);
throw new ConfigurationException("Actual key size of " + keySize + " bits smaller than specified " +
"encryption key length (ESAPI.EncryptionKeyLength) of " + minKeyLen + " bits.");
}
if ( keySize < 112 ) { // NIST Special Pub 800-57 considers 112-bits to be the minimally safe key size from 2010-2030.
// Note that 112 bits 'just happens' to be size of 2-key Triple DES! So for example, if they
// have configured ESAPI's Encryptor.EncryptionKeyLength to (say) 56 bits, we are going to
// nag them like their mother! :)
logger.warning(Logger.SECURITY_FAILURE, "Potentially insecure encryption. Key size of " + keySize + "bits " +
"not sufficiently long for " + cipherAlg + ". Should use appropriate algorithm with key size " +
"of *at least* 112 bits except when required by legacy apps. See NIST Special Pub 800-57.");
}
// Check if algorithm mentioned in SecretKey is same as that being used for Cipher object.
// They should be the same. If they are different, things could fail. (E.g., DES and DESede
// require keys with even parity. Even if key was sufficient size, if it didn't have the correct
// parity it could fail.)
//
String skeyAlg = key.getAlgorithm();
if ( !( cipherAlg.startsWith( skeyAlg + "/" ) || cipherAlg.equals( skeyAlg ) ) ) {
// DISCUSS: Should we thrown a ConfigurationException here or just log a warning??? I'm game for
// either, but personally I'd prefer the squeaky wheel to the annoying throwing of
// a ConfigurationException (which is a RuntimeException). Less likely to upset
// the development community.
logger.warning(Logger.SECURITY_FAILURE, "Encryption mismatch between cipher algorithm (" +
cipherAlg + ") and SecretKey algorithm (" + skeyAlg + "). Cipher will use algorithm " + cipherAlg);
}
byte[] ivBytes = null;
CipherSpec cipherSpec = new CipherSpec(encrypter, keySize); // Could pass the ACTUAL (intended) key size
// Using cipher mode that supports *both* confidentiality *and* authenticity? If so, then
// use the specified SecretKey as-is rather than computing a derived key from it. We also
// don't expect a separate MAC in the specified CipherText object so therefore don't try
// to validate it.
boolean preferredCipherMode = CryptoHelper.isCombinedCipherMode( cipherMode );
SecretKey encKey = null;
if ( preferredCipherMode ) {
encKey = key;
} else {
encKey = computeDerivedKey(KeyDerivationFunction.kdfVersion, getDefaultPRF(),
key, keySize, "encryption");
}
if ( cipherSpec.requiresIV() ) {
String ivType = ESAPI.securityConfiguration().getIVType();
IvParameterSpec ivSpec = null;
if ( ivType.equalsIgnoreCase("random") ) {
ivBytes = ESAPI.randomizer().getRandomBytes(encrypter.getBlockSize());
} else if ( ivType.equalsIgnoreCase("fixed") ) {
String fixedIVAsHex = ESAPI.securityConfiguration().getFixedIV();
ivBytes = Hex.decode(fixedIVAsHex);
/* FUTURE } else if ( ivType.equalsIgnoreCase("specified")) {
// FUTURE - TODO - Create instance of specified class to use for IV generation and
// use it to create the ivBytes. (The intent is to make sure that
// 1) IVs are never repeated for cipher modes like OFB and CFB, and
// 2) to screen for weak IVs for the particular cipher algorithm.
// In meantime, use 'random' for block cipher in feedback mode. Unlikely they will
// be repeated unless you are salting SecureRandom with same value each time. Anything
// monotonically increasing should be suitable, like a counter, but need to remember
// it across JVM restarts. Was thinking of using System.currentTimeMillis(). While
// it's not perfect it probably is good enough. Could even all (advanced) developers
// to define their own class to create a unique IV to allow them some choice, but
// definitely need to provide a safe, default implementation.
*/
} else {
// TODO: Update to add 'specified' once that is supported and added above.
throw new ConfigurationException("Property Encryptor.ChooseIVMethod must be set to 'random' or 'fixed'");
}
ivSpec = new IvParameterSpec(ivBytes);
cipherSpec.setIV(ivBytes);
encrypter.init(Cipher.ENCRYPT_MODE, encKey, ivSpec);
} else {
encrypter.init(Cipher.ENCRYPT_MODE, encKey);
}
logger.debug(Logger.EVENT_SUCCESS, "Encrypting with " + cipherSpec);
byte[] raw = encrypter.doFinal(plaintext);
// Convert to CipherText.
CipherText ciphertext = new CipherText(cipherSpec, raw);
// If we are using a "preferred" cipher mode--i.e., one that supports *both* confidentiality and
// authenticity, there is no point to store a separate MAC in the CipherText object. Thus we only
// do this when we are not using such a cipher mode.
if ( !preferredCipherMode ) {
// Compute derived key, and then use it to compute and store separate MAC in CipherText object.
SecretKey authKey = computeDerivedKey(KeyDerivationFunction.kdfVersion, getDefaultPRF(),
key, keySize, "authenticity");
ciphertext.computeAndStoreMAC( authKey );
}
logger.debug(Logger.EVENT_SUCCESS, "JavaEncryptor.encrypt(SecretKey,byte[],boolean,boolean) -- success!");
success = true; // W00t!!!
return ciphertext;
} catch (InvalidKeyException ike) {
throw new EncryptionException("Encryption failure: Invalid key exception.",
"Requested key size: " + keySize + "bits greater than 128 bits. Must install unlimited strength crypto extension from Sun: " +
ike.getMessage(), ike);
} catch (ConfigurationException cex) {
throw new EncryptionException("Encryption failure: Configuration error. Details in log.", "Key size mismatch or unsupported IV method. " +
"Check encryption key size vs. ESAPI.EncryptionKeyLength or Encryptor.ChooseIVMethod property.", cex);
} catch (InvalidAlgorithmParameterException e) {
throw new EncryptionException("Encryption failure (invalid IV)",
"Encryption problem: Invalid IV spec: " + e.getMessage(), e);
} catch (IllegalBlockSizeException e) {
throw new EncryptionException("Encryption failure (no padding used; invalid input size)",
"Encryption problem: Invalid input size without padding (" + xform + "). " + e.getMessage(), e);
} catch (BadPaddingException e) {
throw new EncryptionException("Encryption failure",
"[Note: Should NEVER happen in encryption mode.] Encryption problem: " + e.getMessage(), e);
} catch (NoSuchAlgorithmException e) {
throw new EncryptionException("Encryption failure (unavailable cipher requested)",
"Encryption problem: specified algorithm in cipher xform " + xform + " not available: " + e.getMessage(), e);
} catch (NoSuchPaddingException e) {
throw new EncryptionException("Encryption failure (unavailable padding scheme requested)",
"Encryption problem: specified padding scheme in cipher xform " + xform + " not available: " + e.getMessage(), e);
} finally {
// Don't overwrite anything in the case of exceptions because they may wish to retry.
if ( success && overwritePlaintext ) {
plain.overwrite(); // Note: Same as overwriting 'plaintext' byte array.
}
}
}
/**
* {@inheritDoc}
*/
public PlainText decrypt(CipherText ciphertext) throws EncryptionException {
// Now more of a convenience function for using the master key.
return decrypt(secretKeySpec, ciphertext);
}
/**
* {@inheritDoc}
*/
public PlainText decrypt(SecretKey key, CipherText ciphertext)
throws EncryptionException, IllegalArgumentException
{
long start = System.nanoTime(); // Current time in nanosecs; used to prevent timing attacks
if ( key == null ) {
throw new IllegalArgumentException("SecretKey arg may not be null");
}
if ( ciphertext == null ) {
throw new IllegalArgumentException("Ciphertext may arg not be null");
}
if ( ! CryptoHelper.isAllowedCipherMode(ciphertext.getCipherMode()) ) {
// This really should be an illegal argument exception, but it could
// mean that a partner encrypted something using a cipher mode that
// you do not accept, so it's a bit more complex than that. Also
// throwing an IllegalArgumentException doesn't allow us to provide
// the two separate error messages or automatically log it.
throw new EncryptionException(DECRYPTION_FAILED,
"Invalid cipher mode " + ciphertext.getCipherMode() +
" not permitted for decryption or encryption operations.");
}
logger.debug(Logger.EVENT_SUCCESS,
"Args valid for JavaEncryptor.decrypt(SecretKey,CipherText): " +
ciphertext);
PlainText plaintext = null;
boolean caughtException = false;
int progressMark = 0;
try {
// First we validate the MAC.
boolean valid = CryptoHelper.isCipherTextMACvalid(key, ciphertext);
if ( !valid ) {
try {
// This is going to fail, but we want the same processing
// to occur as much as possible so as to prevent timing
// attacks. We _could_ just be satisfied by the additional
// sleep in the 'finally' clause, but an attacker on the
// same server who can run something like 'ps' can tell
// CPU time versus when the process is sleeping. Hence we
// try to make this as close as possible. Since we know
// it is going to fail, we ignore the result and ignore
// the (expected) exception.
handleDecryption(key, ciphertext); // Ignore return (should fail).
} catch(Exception ex) {
; // Ignore
}
throw new EncryptionException(DECRYPTION_FAILED,
"Decryption failed because MAC invalid for " +
ciphertext);
}
progressMark++;
// The decryption only counts if the MAC was valid.
plaintext = handleDecryption(key, ciphertext);
progressMark++;
} catch(EncryptionException ex) {
caughtException = true;
String logMsg = null;
switch( progressMark ) {
case 1:
logMsg = "Decryption failed because MAC invalid. See logged exception for details.";
break;
case 2:
logMsg = "Decryption failed because handleDecryption() failed. See logged exception for details.";
break;
default:
logMsg = "Programming error: unexpected progress mark == " + progressMark;
break;
}
logger.error(Logger.SECURITY_FAILURE, logMsg);
throw ex; // Re-throw
}
finally {
if ( caughtException ) {
// The rest of this code is to try to account for any minute differences
// in the time it might take for the various reasons that decryption fails
// in order to prevent any other possible timing attacks. Perhaps it is
// going overboard. If nothing else, if N_SECS is large enough, it might
// deter attempted repeated attacks by making them take much longer.
long now = System.nanoTime();
long elapsed = now - start;
final long NANOSECS_IN_SEC = 1000000000L; // nanosec is 10**-9 sec
long nSecs = N_SECS * NANOSECS_IN_SEC; // N seconds in nano seconds
if ( elapsed < nSecs ) {
// Want to sleep so total time taken is N seconds.
long extraSleep = nSecs - elapsed;
// 'extraSleep' is in nanoseconds. Need to convert to a millisec
// part and nanosec part. Nanosec is 10**-9, millsec is
// 10**-3, so divide by (10**-9 / 10**-3), or 10**6 to
// convert to from nanoseconds to milliseconds.
long millis = extraSleep / 1000000L;
long nanos = (extraSleep - (millis * 1000000L));
// N_SECS is hard-coded so assertion should be okay here.
assert nanos >= 0 && nanos <= Integer.MAX_VALUE :
"Nanosecs out of bounds; nanos = " + nanos;
try {
Thread.sleep(millis, (int)nanos);
} catch(InterruptedException ex) {
; // Ignore
}
} // Else ... time already exceeds N_SECS sec, so do not sleep.
}
}
return plaintext;
}
// Handle the actual decryption portion. At this point it is assumed that
// any MAC has already been validated. (But see "DISCUSS" issue, below.)
private PlainText handleDecryption(SecretKey key, CipherText ciphertext)
throws EncryptionException
{
int keySize = 0;
try {
Cipher decrypter = Cipher.getInstance(ciphertext.getCipherTransformation());
keySize = key.getEncoded().length * 8; // Convert to # bits
// Using cipher mode that supports *both* confidentiality *and* authenticity? If so, then
// use the specified SecretKey as-is rather than computing a derived key from it. We also
// don't expect a separate MAC in the specified CipherText object so therefore don't try
// to validate it.
boolean preferredCipherMode = CryptoHelper.isCombinedCipherMode( ciphertext.getCipherMode() );
SecretKey encKey = null;
if ( preferredCipherMode ) {
encKey = key;
} else {
// TODO: PERFORMANCE: Calculate avg time this takes and consider caching for very short interval
// (e.g., 2 to 5 sec tops). Otherwise doing lots of encryptions in a loop could take a LOT longer.
// But remember Jon Bentley's "Rule #1 on performance: First make it right, then make it fast."
// This would be a security trade-off as it would leave keys in memory a bit longer, so it
// should probably be off by default and controlled via a property.
//
// TODO: Feed in some additional parms here to use as the 'context' for the
// KeyDerivationFunction...especially the KDF version. We would have to
// store that in the CipherText object. We *possibly* could make it
// transient so it would not be serialized with the CipherText object,
// otherwise we would have to implement readObject() and writeObject()
// methods there to support backward compatibility. Anyhow the intent
// is to prevent down grade attacks when we finally re-design and
// re-implement the MAC. Think about this in version 2.1.1.
encKey = computeDerivedKey( ciphertext.getKDFVersion(), ciphertext.getKDF_PRF(),
key, keySize, "encryption");
}
if ( ciphertext.requiresIV() ) {
decrypter.init(Cipher.DECRYPT_MODE, encKey, new IvParameterSpec(ciphertext.getIV()));
} else {
decrypter.init(Cipher.DECRYPT_MODE, encKey);
}
byte[] output = decrypter.doFinal(ciphertext.getRawCipherText());
return new PlainText(output);
} catch (InvalidKeyException ike) {
throw new EncryptionException(DECRYPTION_FAILED, "Must install JCE Unlimited Strength Jurisdiction Policy Files from Sun", ike);
} catch (NoSuchAlgorithmException e) {
throw new EncryptionException(DECRYPTION_FAILED, "Invalid algorithm for available JCE providers - " +
ciphertext.getCipherTransformation() + ": " + e.getMessage(), e);
} catch (NoSuchPaddingException e) {
throw new EncryptionException(DECRYPTION_FAILED, "Invalid padding scheme (" +
ciphertext.getPaddingScheme() + ") for cipher transformation " + ciphertext.getCipherTransformation() +
": " + e.getMessage(), e);
} catch (InvalidAlgorithmParameterException e) {
throw new EncryptionException(DECRYPTION_FAILED, "Decryption problem: " + e.getMessage(), e);
} catch (IllegalBlockSizeException e) {
throw new EncryptionException(DECRYPTION_FAILED, "Decryption problem: " + e.getMessage(), e);
} catch (BadPaddingException e) {
// DISCUSS: This needs fixed. Already validated MAC in CryptoHelper.isCipherTextMACvalid() above.
// So only way we could get a padding exception is if invalid padding were used originally by
// the party doing the encryption. (This might happen with a buggy padding scheme for instance.)
// It *seems* harmless though, so will leave it for now, and technically, we need to either catch it
// or declare it in a throws class. Clearly we don't want to do the later. This should be discussed
// during a code inspection.
SecretKey authKey;
try {
authKey = computeDerivedKey( ciphertext.getKDFVersion(), ciphertext.getKDF_PRF(),
key, keySize, "authenticity");
} catch (Exception e1) {
throw new EncryptionException(DECRYPTION_FAILED,
"Decryption problem -- failed to compute derived key for authenticity: " + e1.getMessage(), e1);
}
boolean success = ciphertext.validateMAC( authKey );
if ( success ) {
throw new EncryptionException(DECRYPTION_FAILED, "Decryption problem: " + e.getMessage(), e);
} else {
throw new EncryptionException(DECRYPTION_FAILED,
"Decryption problem: WARNING: Adversary may have tampered with " +
"CipherText object orCipherText object mangled in transit: " + e.getMessage(), e);
}
}
}
/**
* {@inheritDoc}
*/
public String sign(String data) throws EncryptionException {
try {
Signature signer = Signature.getInstance(signatureAlgorithm);
signer.initSign(privateKey);
signer.update(data.getBytes(encoding));
byte[] bytes = signer.sign();
return ESAPI.encoder().encodeForBase64(bytes, false);
} catch (InvalidKeyException ike) {
throw new EncryptionException("Encryption failure", "Must install unlimited strength crypto extension from Sun", ike);
} catch (Exception e) {
throw new EncryptionException("Signature failure", "Can't find signature algorithm " + signatureAlgorithm, e);
}
}
/**
* {@inheritDoc}
*/
public boolean verifySignature(String signature, String data) {
try {
byte[] bytes = ESAPI.encoder().decodeFromBase64(signature);
Signature signer = Signature.getInstance(signatureAlgorithm);
signer.initVerify(publicKey);
signer.update(data.getBytes(encoding));
return signer.verify(bytes);
} catch (Exception e) {
// NOTE: EncryptionException constructed *only* for side-effect of causing logging.
// FindBugs complains about this and since it examines byte-code, there's no way to
// shut it up.
new EncryptionException("Invalid signature", "Problem verifying signature: " + e.getMessage(), e);
return false;
}
}
/**
* {@inheritDoc}
*
* @param expiration
* @throws IntegrityException
*/
public String seal(String data, long expiration) throws IntegrityException {
if ( data == null ) {
throw new IllegalArgumentException("Data to be sealed may not be null.");
}
try {
String b64data = null;
try {
b64data = ESAPI.encoder().encodeForBase64(data.getBytes("UTF-8"), false);
} catch (UnsupportedEncodingException e) {
; // Ignore; should never happen since UTF-8 built into rt.jar
}
// mix in some random data so even identical data and timestamp produces different seals
String nonce = ESAPI.randomizer().getRandomString(10, EncoderConstants.CHAR_ALPHANUMERICS);
String plaintext = expiration + ":" + nonce + ":" + b64data;
// add integrity check; signature is already base64 encoded.
String sig = this.sign( plaintext );
CipherText ciphertext = this.encrypt( new PlainText(plaintext + ":" + sig) );
String sealedData = ESAPI.encoder().encodeForBase64(ciphertext.asPortableSerializedByteArray(), false);
return sealedData;
} catch( EncryptionException e ) {
throw new IntegrityException( e.getUserMessage(), e.getLogMessage(), e );
}
}
/**
* {@inheritDoc}
*/
public String unseal(String seal) throws EncryptionException {
PlainText plaintext = null;
try {
byte[] encryptedBytes = ESAPI.encoder().decodeFromBase64(seal);
CipherText cipherText = null;
try {
cipherText = CipherText.fromPortableSerializedBytes(encryptedBytes);
} catch( AssertionError e) {
// Some of the tests in EncryptorTest.testVerifySeal() are examples of
// this if assertions are enabled, but otherwise it should not
// normally happen.
throw new EncryptionException("Invalid seal",
"Seal passed garbarge data resulting in AssertionError: " + e);
}
plaintext = this.decrypt(cipherText);
String[] parts = plaintext.toString().split(":");
if (parts.length != 4) {
throw new EncryptionException("Invalid seal", "Seal was not formatted properly.");
}
String timestring = parts[0];
long now = new Date().getTime();
long expiration = Long.parseLong(timestring);
if (now > expiration) {
throw new EncryptionException("Invalid seal", "Seal expiration date of " + new Date(expiration) + " has past.");
}
String nonce = parts[1];
String b64data = parts[2];
String sig = parts[3];
if (!this.verifySignature(sig, timestring + ":" + nonce + ":" + b64data ) ) {
throw new EncryptionException("Invalid seal", "Seal integrity check failed");
}
return new String(ESAPI.encoder().decodeFromBase64(b64data), "UTF-8");
} catch (EncryptionException e) {
throw e;
} catch (Exception e) {
throw new EncryptionException("Invalid seal", "Invalid seal:" + e.getMessage(), e);
}
}
/**
* {@inheritDoc}
*/
public boolean verifySeal( String seal ) {
try {
unseal( seal );
return true;
} catch( EncryptionException e ) {
return false;
}
}
/**
* {@inheritDoc}
*/
public long getTimeStamp() {
return new Date().getTime();
}
/**
* {@inheritDoc}
*/
public long getRelativeTimeStamp( long offset ) {
return new Date().getTime() + offset;
}
// DISCUSS: Why experimental? Would have to be added to Encryptor interface
// but only 3 things I saw wrong with this was 1) it used HMacMD5 instead
// of HMacSHA1 (see discussion below), 2) that the HMac key is the
// same one used for encryption (also see comments), and 3) it caught
// overly broad exceptions. Here it is with these specific areas
// addressed, but no unit testing has been done at this point. -kww
/**
* Compute an HMAC for a String. Experimental.
* @param input The input for which to compute the HMac.
*/
/********************
public String computeHMAC( String input ) throws EncryptionException {
try {
Mac hmac = Mac.getInstance("HMacSHA1"); // DISCUSS: Changed to HMacSHA1. MD5 *badly* broken
// SHA1 should really be avoided, but using
// for HMAC-SHA1 is acceptable for now. Plan
// to migrate to SHA-256 or NIST replacement for
// SHA1 in not too distant future.
// DISCUSS: Also not recommended that the HMac key is the same as the one
// used for encryption (namely, Encryptor.MasterKey). If anything it
// would be better to use Encryptor.MasterSalt for the HMac key, or
// perhaps a derived key based on the master salt. (One could use
// KeyDerivationFunction.computeDerivedKey().)
//
byte[] salt = ESAPI.securityConfiguration().getMasterSalt();
hmac.init( new SecretKeySpec(salt, "HMacSHA1") ); // Was: hmac.init(secretKeySpec)
byte[] inBytes;
try {
inBytes = input.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
logger.warning(Logger.SECURITY_FAILURE, "computeHMAC(): Can't find UTF-8 encoding; using default encoding", e);
inBytes = input.getBytes();
}
byte[] bytes = hmac.doFinal( inBytes );
return ESAPI.encoder().encodeForBase64(bytes, false);
} catch (InvalidKeyException ike) {
throw new EncryptionException("Encryption failure", "Must install unlimited strength crypto extension from Sun", ike);
} catch (NoSuchAlgorithmException e) {
throw new EncryptionException("Could not compute HMAC", "Can't find HMacSHA1 algorithm. " +
"Problem computing HMAC for " + input, e );
}
}
********************/
/**
* Log a security warning every Nth time one of the deprecated encrypt or
* decrypt methods are called. ('N' is hard-coded to be 25 by default, but
* may be changed via the system property
* {@code ESAPI.Encryptor.warnEveryNthUse}.) In other words, we nag
* them until the give in and change it. ;-)
*
* @param where The string "encrypt" or "decrypt", corresponding to the
* method that is being logged.
* @param msg The message to log.
*/
private void logWarning(String where, String msg) {
int counter = 0;
if ( where.equals("encrypt") ) {
counter = encryptCounter++;
where = "JavaEncryptor.encrypt(): [count=" + counter +"]";
} else if ( where.equals("decrypt") ) {
counter = decryptCounter++;
where = "JavaEncryptor.decrypt(): [count=" + counter +"]";
} else {
where = "JavaEncryptor: Unknown method: ";
}
// We log the very first time (note the use of post-increment on the
// counters) and then every Nth time thereafter. Logging every single
// time is likely to be way too much logging.
if ( (counter % logEveryNthUse) == 0 ) {
logger.warning(Logger.SECURITY_FAILURE, where + msg);
}
}
private KeyDerivationFunction.PRF_ALGORITHMS getPRF(String name) {
String prfName = null;
if ( name == null ) {
prfName = ESAPI.securityConfiguration().getKDFPseudoRandomFunction();
} else {
prfName = name;
}
KeyDerivationFunction.PRF_ALGORITHMS prf = KeyDerivationFunction.convertNameToPRF(prfName);
return prf;
}
private KeyDerivationFunction.PRF_ALGORITHMS getDefaultPRF() {
String prfName = ESAPI.securityConfiguration().getKDFPseudoRandomFunction();
return getPRF(prfName);
}
// Private interface to call ESAPI's KDF to get key for encryption or authenticity.
private SecretKey computeDerivedKey(int kdfVersion, KeyDerivationFunction.PRF_ALGORITHMS prf,
SecretKey kdk, 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.
// But this should be OK since this is a private method. Also, this method will
// be called quite often so assertions are a big win as they can be disabled or
// enabled at will.
assert prf != null : "Pseudo Random Function for KDF cannot be null";
assert kdk != 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. NIST says 112-bits is min. If less than that,
// we print warning.
assert keySize >= 56 : "Key has size of " + keySize + ", which is less than absolute minimum of 56-bits.";
assert (keySize % 8) == 0 : "Key size (" + keySize + ") must be a even multiple of 8-bits.";
// However, this one we want as a runtime check because we don't have this check
// in KeyDerivationFunction.computeDerivedKey() as we want that method
// to be more general.
if ( !( purpose.equals("encryption") || purpose.equals("authenticity") ) ) {
String exMsg = "Programming error in ESAPI?? 'purpose' for computeDerivedKey() must be \"encryption\" or \"authenticity\".";
throw new EncryptionException(exMsg, exMsg);
}
KeyDerivationFunction kdf = new KeyDerivationFunction(prf);
if ( kdfVersion != 0 ) {
kdf.setVersion(kdfVersion);
}
return kdf.computeDerivedKey(kdk, keySize, purpose);
}
// Get all the algorithms we will be using from ESAPI.properties.
private static void setupAlgorithms() {
// setup algorithms
encryptAlgorithm = ESAPI.securityConfiguration().getEncryptionAlgorithm();
signatureAlgorithm = ESAPI.securityConfiguration().getDigitalSignatureAlgorithm();
randomAlgorithm = ESAPI.securityConfiguration().getRandomAlgorithm();
hashAlgorithm = ESAPI.securityConfiguration().getHashAlgorithm();
hashIterations = ESAPI.securityConfiguration().getHashIterations();
encoding = ESAPI.securityConfiguration().getCharacterEncoding();
encryptionKeyLength = ESAPI.securityConfiguration().getEncryptionKeyLength();
signatureKeyLength = ESAPI.securityConfiguration().getDigitalSignatureKeyLength();
}
// Set up signing key pair using the master password and salt. Called (once)
// from the JavaEncryptor CTOR.
private static void initKeyPair(SecureRandom prng) throws NoSuchAlgorithmException {
String sigAlg = signatureAlgorithm.toLowerCase();
if ( sigAlg.endsWith("withdsa") ) {
//
// Admittedly, this is a kludge. However for Sun JCE, even though
// "SHA1withDSA" is a valid signature algorithm name, if one calls
// KeyPairGenerator kpg = KeyPairGenerator.getInstance("SHA1withDSA");
// that will throw a NoSuchAlgorithmException with an exception
// message of "SHA1withDSA KeyPairGenerator not available". Since
// SHA1withDSA and DSA keys should be identical, we use "DSA"
// in the case that SHA1withDSA or SHAwithDSA was specified. This is
// all just to make these 2 work as expected. Sigh. (Note:
// this was tested with JDK 1.6.0_21, but likely fails with earlier
// versions of the JDK as well. Haven't experimented with later
// versions.)
//
sigAlg = "DSA";
} else if ( sigAlg.endsWith("withrsa") ) {
// Ditto for RSA.
sigAlg = "RSA";
}
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(sigAlg);
keyGen.initialize(signatureKeyLength, prng);
KeyPair pair = keyGen.generateKeyPair();
privateKey = pair.getPrivate();
publicKey = pair.getPublic();
}
}