package org.limewire.xmpp.client.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import org.limewire.security.certificate.CipherProvider; /** * This class encrypts/decrypts from/to a plaintext password using * symmetric key encryption (AES). It does the following: * <p> * Given a plaintext password: * <pre> * 1. Generate symmetric encryption Key * 2. Encrypt password * 3. Store both key and password together, base64 encoded: * base64 encoding ([1 byte key length][encryption key][1 byte passwd length][encrypted passwd]) * </pre> * Given an encrypted block of data (key and password): * <pre> * 1. Base64 decode the encoded data * 2. Read in the key length * 3. Read in the key * 4. Use the key to decrypt the encrypted password, and return the password * </pre> * */ public final class Password { private static final CipherProvider.CipherType CIPHER_TYPE = CipherProvider.CipherType.AES; private static final String DEFAULT_ENCODING = "UTF-8"; /** * The password to decrypt/encrypt * When isEncrypted is true, this represents the encrypted password * Otherwise, this is the plaintext password */ private final String passwordString; private final boolean isEncrypted; private CipherProvider cipherProvider; Password(CipherProvider cipherProvider, String passwordString, boolean isEncrypted) { this.cipherProvider = cipherProvider; this.passwordString = passwordString; this.isEncrypted = isEncrypted; } public String encryptPassword() throws IOException, NoSuchAlgorithmException { if (isEncrypted) { throw new IllegalStateException("Password is already encrypted; Cannot encrypt."); } KeyGenerator kgen = KeyGenerator.getInstance(CIPHER_TYPE.getDescription()); SecretKey key = kgen.generateKey(); byte[] keyAsBytes = key.getEncoded(); byte[] encrypted = cipherProvider.encrypt( passwordString.getBytes(DEFAULT_ENCODING), key, CIPHER_TYPE); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeField(baos, keyAsBytes); writeField(baos, encrypted); byte[] magicBytes = baos.toByteArray(); return new String(Base64.encodeBase64(magicBytes), DEFAULT_ENCODING); } public String decryptPassword() throws IOException, GeneralSecurityException { if (!isEncrypted) { throw new IllegalStateException("Password is not encrypted; Cannot decrypt."); } byte[] magicBytes = Base64.decodeBase64(passwordString.getBytes(DEFAULT_ENCODING)); ByteArrayInputStream bais = new ByteArrayInputStream(magicBytes); byte[] keyAsBytes; byte[] encryptedPassword; keyAsBytes = readField(bais); encryptedPassword = readField(bais); SecretKeySpec keySpec = new SecretKeySpec(keyAsBytes, CIPHER_TYPE.getDescription()); byte[] pwdAsBytes = cipherProvider.decrypt(encryptedPassword, keySpec, CIPHER_TYPE); // should have reached the end - any extraneous bytes indicates corrupted bytes if (bais.available() > 0) { throw new IOException("Additional bytes after encryption key"); } return new String(pwdAsBytes, DEFAULT_ENCODING); } private static void writeField(ByteArrayOutputStream baos, byte[] data) throws IOException { baos.write(data.length); baos.write(data); } private static byte[] readField(ByteArrayInputStream bais) throws IOException { // read length of key int fieldStatedLen = bais.read(); if (fieldStatedLen <= 0) { throw new IOException("Corrupt key detected"); } byte[] fieldBytes = new byte[fieldStatedLen]; int actualFieldLen; try { actualFieldLen = bais.read(fieldBytes); } catch (IOException e) { throw new IOException("Corrupt key detected: " + e.getMessage()); } if (actualFieldLen != fieldStatedLen) { throw new IOException("Corrupt key detected: Mismatch between " + "stated key length and actual key length."); } return fieldBytes; } }