// // Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s). // All rights reserved. // package openadk.library; import java.io.IOException; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.InvalidKeyException; import java.security.InvalidAlgorithmParameterException; import javax.crypto.NoSuchPaddingException; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.RC2ParameterSpec; import openadk.library.infra.Password; import openadk.library.infra.PasswordAlgorithm; import java.security.spec.AlgorithmParameterSpec; import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder; /** * Provides easy read and write access to encrypted passwords in the * Authetication(US) or Identity(UK) objects * <p> * This class is abstract. Concrete instances can be obtained by calling the * appropriate <code>getInstance</code> overload. * </p> * <p> * The SIFEncryption class uses the following properties to determine its default * algorithm and key for writing and for finding keys for decrypting passwords. * Please see <see cref="AgentProperties"/> for details * </p> * <table border="1" cellpadding="2" cellspacing="3"> * <tr> * <td><center><b>Property</b></center></td> * <td><b>Description</b></td> * </tr> * <tr> * <td><b><code>adk.encryption.algorithm</code></b></td> * <td>The default algorithm used for writing passwords</td> * </tr> * <tr> * <td><b><code>adk.encryption.key</code></b></td> * <td>The name of the default key to use for encryption</td> * </tr> * <tr> * <td><b><code>adk.encryption.keys.[keyname]</code></b></td> * <td>The actual key to use for encryption or decryption where "keyname" * matches the @KeyName attribute of the AuthenticationInfoPassword object</td> * </tr> * </table> * <p> * For example usage, please see the AuthenticationProvider and * AuthenticationSubscriber example projects. * </p> * @author Andrew Elmhorst * @version ADK 1.5.1 */ public abstract class SIFEncryption { /** * The algorithm that this class was initialized with */ private PasswordAlgorithm fAlgorithm; /** * The key name that this class was initialized with. This matches the * @KeyName attribute of the <code>AuthenticationInfoPassword</code> class */ private String fkeyName; /** * The current instance of the class that is in use. The instance * may be reused between objects, as long as the algorithm and KeyName * remain the same. */ private static SIFEncryption sCurrentInstance; /** * Used for encoding binary data to BASE64 */ protected BASE64Encoder fEncoder = new BASE64Encoder(); /** * Used for decoding binary data from BASE64 */ protected BASE64Decoder fDecoder = new BASE64Decoder(); /** * Instances of this class can only be created by calls to the getInstance() methods * @param algorithm The encryption or hashing algorithm to use * @param keyName The name of the key being used */ protected SIFEncryption( PasswordAlgorithm algorithm, String keyName ) { fkeyName = keyName; fAlgorithm = algorithm; } /** * Creates an instance of SIFEncryption that uses the specified PasswordAlgorithm, * keyName and key. * * @param algorithm The algorithm to use for encrypting or decrypting passwords * @param keyName The name of the encryption key to use. * @param key The encryption key to use. This parameter is ignored for SHA1 * and MD5 because they are not keyed hash algorithms. It's also ignore for BASE64 * @return an instance of the SIFEncryption class that can read and write passwords using * the chosen algorithm and key. * @throws NoSuchAlgorithmException Thrown if the specified algorithm is not available. For example, * RC2 and RSA are not available in the default java 1.4 cipher suites. * @throws NoSuchPaddingException Thrown if the padding method (which is always PKCS5Padding) is not available */ public static synchronized SIFEncryption getInstance( PasswordAlgorithm algorithm, String keyName, byte[] key ) throws NoSuchAlgorithmException, NoSuchPaddingException { if ( sCurrentInstance != null ) { if( sCurrentInstance.getAlgorithm().equals( algorithm ) && ( sCurrentInstance.getKeyName().equals( keyName ) || sCurrentInstance.getKey() == null ) ) { return sCurrentInstance; } else { sCurrentInstance = null; } } // Base64 is not supported in all SIF locales, and thus is not looked // up using the PasswordAlgorithm enum if ( algorithm.valueEquals( "base64" ) ) { sCurrentInstance = new SIFClearTextEncryption( algorithm, keyName ); } else if ( algorithm.equals( PasswordAlgorithm.SHA1 )) { sCurrentInstance = new SIFHashEncryption( algorithm, keyName, MessageDigest.getInstance( "SHA1" ) ); } else if ( algorithm.equals( PasswordAlgorithm.MD5 )) { sCurrentInstance = new SIFHashEncryption( algorithm, keyName, MessageDigest.getInstance( "MD5" ) ); } else if ( algorithm.equals(PasswordAlgorithm.DES )) { sCurrentInstance = new SIFSymmetricEncryption( algorithm, keyName, "DES", key ); } else if ( algorithm.equals(PasswordAlgorithm.TRIPLEDES )) { sCurrentInstance = new SIFSymmetricEncryption( algorithm, keyName, "DESede", key ); } else if ( algorithm.equals( PasswordAlgorithm.RC2 )) { sCurrentInstance = new SIFSymmetricEncryption( algorithm, keyName, "RC2", key ); } else { throw new ADKNotSupportedException( "Encryption algorithm " + algorithm + " is not supported." ); } return sCurrentInstance; } /** * Creates an instance of SIFEncryption that can decrypt * the password field automatically, using settings * defined in the agent's properties. * <p> * This method searches the agent properties in effect * for the zone and looks for one that matches the key * defined in the AuthenticationInfoPassword object. * If it finds one, it returns an instance of SIFEncryption * that has been initialized with the proper key and * encryption algorithm for the field. * <p> * This method looks for a property named * <code>adk.encryption.keys.[keyName]</code> where <code>[keyName]</code> is the name * of the key field defined by the AuthenticationInfoPassword * field. * @param password The password object that needs to be decrypted * @param zone The zone that is in scope for the current message * @return An instance of SIFEncryption that can read the password from the given A * uthenticationInfoPassword object * @throws IOException If the key stored in the agent properties cannot be converted * from BASE64 to binary * @throws NoSuchAlgorithmException Thrown if the specified algorithm is not available. * For example, RC2 and RSA are not available in the default java 1.4 cipher suites. * @throws NoSuchPaddingException Thrown if the padding method * (which is always PKCS5Padding) is not available */ public static SIFEncryption getInstance( Password password, Zone zone ) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException { if ( sCurrentInstance != null && sCurrentInstance.getAlgorithm().equals( password.getAlgorithm() ) && ( sCurrentInstance.getKeyName().equals( password.getKeyName() ) || sCurrentInstance.getKey() == null ) ) { return sCurrentInstance; } byte[] key = zone.getProperties().getEncryptionKey( password.getKeyName() ); return getInstance( PasswordAlgorithm.wrap( password.getAlgorithm() ), password.getKeyName(), key ); } /** * Creates an instance of SIFEncryption that can be used * for writing the AuthenticationInfoPassword field, * using settings from the agent's properties * <p> * This method searches for two properties in the agent * properties. <code>adk.encryption.algorithm</code> returns the default * algorithm the agent uses for encryption. * <code>adk.encryption.key</code> returns the name of the key to use for * encryption, which, if required, will be read from the * <code>adk.encryption.keys.[keyName]</code> property. The * <code>adk.encryption.keys.[keyName]</code> property is not used or required * for base64, SHA1, and MD5. * <p> * The <code>adk.encryption.key</code> property is required * for encryption methods that use a key. It is this value that will be written * to the @KeyName attribute of the AuthenticationInfoPassword object. * @param zone the zone that is in scope for the current message * @return an instance of SIFEncryption that can write or read passwords using the default * settings. * @throws ADKException If the agent properties do not contain a default encryption * algorithm or key. The <code>adk.encryption.key</code> property is required * for encryption methods that use a key. It is this value that will be written * to the @KeyName attribute of the AuthenticationInfoPassword object. * @throws IOException If the key stored in the agent properties cannot be converted * from BASE64 to binary * @throws NoSuchAlgorithmException Thrown if the specified algorithm is not available. * For example, RC2 and RSA are not available in the default java 1.4 cipher suites. * @throws NoSuchPaddingException Thrown if the padding method * (which is always PKCS5Padding) is not available */ public static SIFEncryption getInstance( Zone zone ) throws ADKException, IOException, NoSuchPaddingException, NoSuchAlgorithmException { String alg = zone.getProperties().getDefaultEncryptionAlgorithm(); String keyName = zone.getProperties().getDefaultEncryptionKeyName(); if( alg == null ) { throw new ADKException( "The default encryption algorithm or default key name is not defined in the agent or zone properties", zone ); } byte[] key = null; if( keyName != null ) { key = zone.getProperties().getEncryptionKey( keyName ); } // Don't check for a null key at this time because some of the algorithms don't even need a key return getInstance( PasswordAlgorithm.wrap( alg ), keyName, key ); } /** * Encrypts the specified password and populates the * AuthenticationInfoPassword field with the algorithm and key name * values. * @param password The password object to write the encrypted password to * @param value The plain-text password to write to the object * @throws IOException If the value cannot be encoded to base64 * @throws InvalidKeyException If the key provided is not valid for the cipher * algorithm * @throws IllegalBlockSizeException * @throws BadPaddingException */ public void writePassword( Password password, String value ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { password.setAlgorithm( fAlgorithm ); password.setKeyName( fkeyName ); } /** * Returns the unencrypted password value from the * AuthenticationInfoPassword field. * <p> * If the algorithm in use is a hash algorithm, the Base64 * instance of the hash will be returned instead. The application * can check the {@link #isHash()} method to determine of the value * being returned is a hash value or the plain-text password. * @param password The password object to read the password value from * @return The plain-text password or hash value * @throws IOException If a decryption error occurs * @throws InvalidKeyException If a decryption error occurs * @throws IllegalBlockSizeException If a decryption error occurs * @throws BadPaddingException If a decryption error occurs * @throws InvalidAlgorithmParameterException If a decryption error occurs */ public abstract String readPassword( Password password ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException; /** * Returns the encryption algorithm that is in use by the class * @return the encryption algorithm that is currently being used */ public PasswordAlgorithm getAlgorithm() { return fAlgorithm; } /** * Returns the name of the key that is currently being used. This value is the same * as the {@link Password#getKeyName()} * attribute. * @return the name of the key that is currently being used */ public String getKeyName() { return fkeyName; } /** * Returns the key that the class is currently using to encrypt or decrypt passwords * @return the encryption key */ public abstract byte[] getKey(); /** * Returns true if the value is a hashed value and cannot be decrypted. In this case, * the {@link #readPassword(Password)} method will return the hashed value as a Base64 string * @return true if the password is a hashed value. false if it is a plain-text password * that can be decrypted */ public abstract boolean isHash(); private static class SIFSymmetricEncryption extends SIFEncryption { private Cipher fCipher; private SecretKeySpec fKeySpec; private byte[] fKey; SIFSymmetricEncryption( PasswordAlgorithm algorithm, String keyName, String cipherAlgorithm, byte[] key ) throws NoSuchAlgorithmException, NoSuchPaddingException { super( algorithm, keyName ); fKey = key; fCipher = Cipher.getInstance( cipherAlgorithm + "/CBC/PKCS5Padding" ); fKeySpec = new SecretKeySpec( key, cipherAlgorithm ); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#writePassword(openadk.library.infra.AuthenticationInfoPassword, java.lang.String) */ @Override public void writePassword( Password password, String value ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { super.writePassword( password, value); fCipher.init( Cipher.ENCRYPT_MODE, fKeySpec ); byte[] source = value.getBytes( "UTF-8" ); byte[] encryptedPassword = fCipher.doFinal( source ); ByteBuffer finalValue = ByteBuffer.allocate( 8 + encryptedPassword.length ); finalValue.put( fCipher.getIV() ); finalValue.put( encryptedPassword ); password.setTextValue( fEncoder.encode(finalValue.array() ) ); } @Override public String readPassword( Password password ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { byte[] encryptedValue = fDecoder.decodeBuffer( password.getTextValue() ); // Initialize the IV parameter AlgorithmParameterSpec params = null; if( this.getAlgorithm().equals( PasswordAlgorithm.RC2 ) ) { ByteBuffer iv = ByteBuffer.allocate( 8 ); iv.put( encryptedValue, 0, 8 ); params = new RC2ParameterSpec( fKey.length * 8 , iv.array() ); } else { params = new IvParameterSpec( encryptedValue, 0, 8 ); } fCipher.init( Cipher.DECRYPT_MODE, fKeySpec, params ); byte[] decryptedPassword = fCipher.doFinal( encryptedValue, 8, encryptedValue.length - 8 ); return new String( decryptedPassword, "UTF-8" ); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#getKey() */ @Override public byte[] getKey() { return fKey; } /* (non-Javadoc) * @see openadk.library.SIFEncryption#isHash() */ @Override public boolean isHash() { return false; } } /** * Encodes and decodes passwords using BASE64 * @author Andrew * */ private static class SIFClearTextEncryption extends SIFEncryption { SIFClearTextEncryption( PasswordAlgorithm algorithm, String keyName ){ super( algorithm, "" ); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#writePassword(openadk.library.infra.AuthenticationInfoPassword, java.lang.String) */ @Override public void writePassword( Password password, String value ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { super.writePassword( password, value); byte[] clearTextPassword = value.getBytes( "UTF-8" ); password.setTextValue( fEncoder.encode( clearTextPassword ) ); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#readPassword(openadk.library.infra.AuthenticationInfoPassword) */ @Override public String readPassword( Password password ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { byte[] clearTextPassword = fDecoder.decodeBuffer( password.getTextValue() ); return new String( clearTextPassword, "UTF-8" ); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#getKey() */ @Override public byte[] getKey() { return null; } /* (non-Javadoc) * @see openadk.library.SIFEncryption#isHash() */ @Override public boolean isHash() { return false; } } /** * Encodes passwords using a hashing algorithm * @author Andrew */ private static class SIFHashEncryption extends SIFEncryption { private MessageDigest fDigestAlgorithm; SIFHashEncryption( PasswordAlgorithm algorithm, String keyName, MessageDigest alg ) { super( algorithm, "" ); fDigestAlgorithm = alg; } /* (non-Javadoc) * @see openadk.library.SIFEncryption#writePassword(openadk.library.infra.AuthenticationInfoPassword, java.lang.String) */ @Override public void writePassword( Password password, String value ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { super.writePassword( password, value); byte[] pass = value.getBytes( "UTF-8" ); byte[] hashedPassword = fDigestAlgorithm.digest( pass ); password.setTextValue( fEncoder.encode( hashedPassword )); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#readPassword(openadk.library.infra.AuthenticationInfoPassword) */ @Override public String readPassword( Password password ) throws IOException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { return password.getTextValue(); } /* (non-Javadoc) * @see openadk.library.SIFEncryption#getKey() */ @Override public byte[] getKey() { return null; } /* (non-Javadoc) * @see openadk.library.SIFEncryption#isHash() */ @Override public boolean isHash() { return true; } } }