org.owasp.esapi.crypto.CipherSpec 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) 2009 - The OWASP Foundation
*/
// If we had PRIVATE packages, e.g., org.owasp.esapi.util.pvt, this would belong
// there. CipherText uses this, but doesn't expose it directly.
package org.owasp.esapi.crypto;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import javax.crypto.Cipher;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.StringUtilities;
import org.owasp.esapi.util.NullSafe;
/**
* Specifies all the relevant configuration data needed in constructing and
* using a {@link javax.crypto.Cipher} except for the encryption key.
*
* The "setters" all return a reference to {@code this} so that they can be
* strung together.
*
* Note: While this is a useful class in it's own right, it should primarily be
* regarded as an implementation class to use with ESAPI encryption, especially
* the reference implementation. It is not intended to be used directly
* by application developers, but rather only by those either extending ESAPI
* or in the ESAPI reference implementation. Use directly by application
* code is not recommended or supported.
*
* @author [email protected]
* @since 2.0
*/
public final class CipherSpec implements Serializable {
private static final long serialVersionUID = 20090822; // version, in YYYYMMDD format
private String cipher_xform_ = ESAPI.securityConfiguration().getCipherTransformation();
private int keySize_ = ESAPI.securityConfiguration().getEncryptionKeyLength(); // In bits
private int blockSize_ = 16; // In bytes! I.e., 128 bits!!!
private byte[] iv_ = null;
private boolean blockSizeExplicitlySet = false; // Used for check in setIV().
// Cipher transformation component. Format is ALG/MODE/PADDING
private enum CipherTransformationComponent { ALG, MODE, PADDING }
/**
* CTOR that explicitly sets everything.
* @param cipherXform The cipher transformation
* @param keySize The key size (in bits).
* @param blockSize The block size (in bytes).
* @param iv The initialization vector. Null if not applicable.
*/
public CipherSpec(String cipherXform, int keySize, int blockSize, final byte[] iv) {
setCipherTransformation(cipherXform);
setKeySize(keySize);
setBlockSize(blockSize);
setIV(iv);
}
/**
* CTOR that sets everything but IV.
* @param cipherXform The cipher transformation
* @param keySize The key size (in bits).
* @param blockSize The block size (in bytes).
*/
public CipherSpec(String cipherXform, int keySize, int blockSize) {
// Note: Do NOT use
// this(cipherXform, keySize, blockSize, null);
// because of checks in setIV().
//
setCipherTransformation(cipherXform);
setKeySize(keySize);
setBlockSize(blockSize);
}
/** CTOR that sets everything but block size and IV. */
public CipherSpec(String cipherXform, int keySize) {
setCipherTransformation(cipherXform);
setKeySize(keySize);
}
/** CTOR that sets everything except block size. */
public CipherSpec(String cipherXform, int keySize, final byte[] iv) {
setCipherTransformation(cipherXform);
setKeySize(keySize);
setIV(iv);
}
/** CTOR that sets everything except for the cipher key size and possibly
* the IV. (IV may not be applicable--e.g., with ECB--or may not have
* been specified yet.
*/
public CipherSpec(final Cipher cipher) {
setCipherTransformation(cipher.getAlgorithm(), true);
setBlockSize(cipher.getBlockSize());
if ( cipher.getIV() != null ) {
setIV(cipher.getIV());
}
}
/** CTOR that sets everything. */
public CipherSpec(final Cipher cipher, int keySize) {
this(cipher);
setKeySize(keySize);
}
/* CTOR that sets only the IV and uses defaults for everything else. */
public CipherSpec(final byte[] iv) {
setIV(iv);
}
/**
* Default CTOR. Creates a cipher specification for 128-bit cipher
* transformation of "AES/CBC/PKCS5Padding" and a {@code null} IV.
*/
public CipherSpec() {
// All defaults
}
/**
* Set the cipher transformation for this {@code CipherSpec}.
* @param cipherXform The cipher transformation string; e.g., "DESede/CBC/PKCS5Padding".
* @return This current {@code CipherSpec} object.
*/
public CipherSpec setCipherTransformation(String cipherXform) {
setCipherTransformation(cipherXform, false);
return this;
}
/**
* Set the cipher transformation for this {@code CipherSpec}. This is only
* used by the CTOR {@code CipherSpec(Cipher)} and {@code CipherSpec(Cipher, int)}.
* @param cipherXform The cipher transformation string; e.g.,
* "DESede/CBC/PKCS5Padding". May not be null or empty.
* @param fromCipher If true, the cipher transformation was set via
* {@code Cipher.getAlgorithm()} which may only return the
* actual algorithm. In that case we check and if all 3 parts
* were not specified, then we specify the parts that were
* based on "ECB" as the default cipher mode and "NoPadding"
* as the default padding scheme.
* @return This current {@code CipherSpec} object.
*/
private CipherSpec setCipherTransformation(String cipherXform, boolean fromCipher) {
if ( ! StringUtilities.notNullOrEmpty(cipherXform, true) ) { // Yes, really want '!' here.
throw new IllegalArgumentException("Cipher transformation may not be null or empty string (after trimming whitespace).");
}
int parts = cipherXform.split("/").length;
// Assertion should be okay here as these conditions are checked
// elsewhere and this method is private.
assert ( !fromCipher ? (parts == 3) : true ) :
"Malformed cipherXform (" + cipherXform + "); must have form: \"alg/mode/paddingscheme\"";
if ( fromCipher && (parts != 3) ) {
// Indicates cipherXform was set based on Cipher.getAlgorithm()
// and thus may not be a *complete* cipher transformation.
if ( parts == 1 ) {
// Only algorithm was given.
cipherXform += "/ECB/NoPadding";
} else if ( parts == 2 ) {
// Only algorithm and mode was given.
cipherXform += "/NoPadding";
} else if ( parts == 3 ) {
// All three parts provided. Do nothing. Could happen if not compiled with
// assertions enabled, but there are explicit checks elsewhere.
; // Do nothing - shown only for completeness.
} else {
// Should never happen unless Cipher implementation is totally screwed up.
throw new IllegalArgumentException("Cipher transformation '" +
cipherXform + "' must have form \"alg/mode/paddingscheme\"");
}
} else if ( !fromCipher && parts != 3 ) {
throw new IllegalArgumentException("Malformed cipherXform (" + cipherXform +
"); must have form: \"alg/mode/paddingscheme\"");
}
// Assertion should also be okay here as these conditions are checked
// elsewhere and this method is private.
assert cipherXform.split("/").length == 3 : "Implementation error setCipherTransformation()";
this.cipher_xform_ = cipherXform;
return this;
}
/**
* Get the cipher transformation.
* @return The cipher transformation {@code String}.
*/
public String getCipherTransformation() {
return cipher_xform_;
}
/**
* Set the key size for this {@code CipherSpec}.
* @param keySize The key size, in bits. Must be positive integer.
* @return This current {@code CipherSpec} object.
*/
public CipherSpec setKeySize(int keySize) {
if ( keySize <= 0 ) {
throw new IllegalArgumentException("keySize must be > 0; keySize=" + keySize);
}
this.keySize_ = keySize;
return this;
}
/**
* Retrieve the key size, in bits.
* @return The key size, in bits, is returned.
*/
public int getKeySize() {
return keySize_;
}
/**
* Set the block size for this {@code CipherSpec}.
* @param blockSize The block size, in bytes. Must be positive integer appropriate
* for the specified cipher algorithm.
* @return This current {@code CipherSpec} object.
*/
public CipherSpec setBlockSize(int blockSize) {
if ( blockSize <= 0 ) {
throw new IllegalArgumentException("blockSize must be > 0; blockSize=" + blockSize);
}
this.blockSize_ = blockSize;
blockSizeExplicitlySet = true;
return this;
}
/**
* Retrieve the block size, in bytes.
* @return The block size, in bytes, is returned.
*/
public int getBlockSize() {
return blockSize_;
}
/**
* Retrieve the cipher algorithm.
* @return The cipher algorithm.
*/
public String getCipherAlgorithm() {
return getFromCipherXform(CipherTransformationComponent.ALG);
}
/**
* Retrieve the cipher mode.
* @return The cipher mode.
*/
public String getCipherMode() {
return getFromCipherXform(CipherTransformationComponent.MODE);
}
/**
* Retrieve the cipher padding scheme.
* @return The padding scheme is returned.
*/
public String getPaddingScheme() {
return getFromCipherXform(CipherTransformationComponent.PADDING);
}
/**
* Retrieve the initialization vector (IV).
* @return The IV as a byte array.
*/
public byte[] getIV() {
return iv_;
}
/**
* Set the initialization vector (IV).
* @param iv The byte array to set as the IV. A copy of the IV is saved.
* This parameter is ignored if the cipher mode does not
* require an IV.
* @return This current {@code CipherSpec} object.
*/
public CipherSpec setIV(final byte[] iv) {
if ( ! ( requiresIV() && (iv != null && iv.length != 0) ) ) {
throw new IllegalArgumentException("Required IV cannot be null or 0 length.");
}
// Don't store a reference, but make a copy! When an IV is provided, it generally should
// be the same length as the block size of the cipher.
if ( iv != null ) { // Allow null IV for ECB mode.
// 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, 9, 10, 11, 12, or 13 octets because of
// it's formatting function.
/***
if ( iv.length != this.getBlockSize() && blockSizeExplicitlySet ) {
throw new IllegalArgumentException("IV must be same length as cipher block size (" +
this.getBlockSize() + " bytes)");
}
***/
iv_ = new byte[ iv.length ];
CryptoHelper.copyByteArray(iv, iv_);
}
return this;
}
/**
* Return true if the cipher mode requires an IV.
* @return True if the cipher mode requires an IV, otherwise false.
* */
public boolean requiresIV() {
String cm = getCipherMode();
// Add any other cipher modes supported by JCE but not requiring IV.
// ECB is the only one I'm aware of that doesn't. Mode is not case
// sensitive.
if ( "ECB".equalsIgnoreCase(cm) ) {
return false;
}
return true;
}
/**
* Override {@code Object.toString()} to provide something more useful.
* @return A meaningful string describing this object.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder("CipherSpec: ");
sb.append( getCipherTransformation() ).append("; keysize= ").append( getKeySize() );
sb.append(" bits; blocksize= ").append( getBlockSize() ).append(" bytes");
byte[] iv = getIV();
String ivLen = null;
if ( iv != null ) {
ivLen = "" + iv.length; // Convert length to a string
} else {
ivLen = "[No IV present (not set or not required)]";
}
sb.append("; IV length = ").append( ivLen ).append(" bytes.");
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 CipherSpec) {
CipherSpec that = (CipherSpec)other;
result = (that.canEqual(this) &&
NullSafe.equals(this.cipher_xform_, that.cipher_xform_) &&
this.keySize_ == that.keySize_ &&
this.blockSize_ == that.blockSize_ &&
// In all versions of JDK 7 and later, comparison safe from timing attacks.
java.security.MessageDigest.isEqual(this.iv_, that.iv_) );
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
StringBuilder sb = new StringBuilder();
sb.append( getCipherTransformation() );
sb.append( "" + getKeySize() );
sb.append( "" + getBlockSize() );
byte[] iv = getIV();
if ( iv != null && iv.length > 0 ) {
String ivStr = null;
try {
ivStr = new String(iv, "UTF-8");
}
catch(UnsupportedEncodingException ex) {
// Should never happen as UTF-8 encode supported by rt.jar,
// but it it does, just use default encoding.
ivStr = new String(iv);
}
sb.append( ivStr );
}
return sb.toString().hashCode();
}
/**
* Needed for correct definition of equals for general classes.
* (Technically not needed for 'final' classes 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
* How to write an Equality Method in Java
* for full explanation.
*
*/
protected boolean canEqual(Object other) {
return (other instanceof CipherSpec);
}
/**
* Split the current cipher transformation and return the requested part.
* @param component The component of the cipher transformation to return.
* @return The cipher algorithm, cipher mode, or padding, as requested.
*/
private String getFromCipherXform(CipherTransformationComponent component) {
int part = component.ordinal();
String[] parts = getCipherTransformation().split("/");
// Assertion should also be okay here as these conditions are checked
// elsewhere and this method is private.
assert parts.length == 3 : "Invalid cipher transformation: " + getCipherTransformation();
return parts[part];
}
}