package org.subethamail.core.admin; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.logging.Level; import javax.annotation.PostConstruct; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.ejb.EJBException; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.inject.Inject; import javax.inject.Singleton; import lombok.extern.java.Log; import org.apache.commons.codec.binary.Base64; import org.subethamail.common.NotFoundException; import org.subethamail.core.admin.i.Encryptor; import org.subethamail.core.admin.i.ExpiredException; import org.subethamail.core.util.SubEtha; import org.subethamail.core.util.SubEthaEntityManager; import org.subethamail.entity.Config; /** * Performs encryption and decryption using a constant key. The * key is set as a config value. If the key doesn't exist when * the service is started, a random key is generated. * * Note that this bean does NOT have a remote interface. * * @author Jeff Schnitzer * @author Scott Hernandez */ @Singleton @TransactionAttribute(TransactionAttributeType.REQUIRED) @Log public class EncryptorBean implements Encryptor { /** * The name of the config value that holds the current encryption key. * The actual config value will be a base64-encoded string. */ private static final String KEY_CONFIG_ID = "cipherKey"; /** * The number of bytes in a key. */ private static final int KEY_LENGTH = 16; /** * An initial vector for encoding. The content is more or less irrelevant. */ private static final byte[] IV = new byte[KEY_LENGTH]; static { for (int i=0; i<KEY_LENGTH; i++) IV[i] = (byte)(i+10); } /** */ @Inject @SubEtha SubEthaEntityManager em; /* */ @PostConstruct public void start() throws Exception { log.log(Level.FINE,"Starting EncryptorBean, entitymanager is {0}", em); // If we don't already have a key, generate one try { Config cfg = this.em.get(Config.class, KEY_CONFIG_ID); // Might as well sanity check it String value = (String)cfg.getValue(); if (value == null || value.length() == 0) cfg.setValue(this.generateKey()); } catch (NotFoundException ex) { log.log(Level.INFO,"Creating new cypher key for Encryptor, and storing it in the database"); Config cfg = new Config(KEY_CONFIG_ID, this.generateKey()); this.em.persist(cfg); } } /** * Randomly generates a new, base64-encoded key. The raw key * will be 16 bytes long. */ protected String generateKey() { byte[] generated = new byte[KEY_LENGTH]; Random rnd = new SecureRandom(); rnd.nextBytes(generated); return new String(Base64.encodeBase64(generated)); } /** * @return the current key from the config */ protected byte[] getKey() { String base64 = (String)this.em.findConfigValue(KEY_CONFIG_ID); return Base64.decodeBase64(base64.getBytes()); } /* */ public byte[] encrypt(byte[] plainText) { log.log(Level.FINE,"Encrypting {0} bytes", plainText.length); try { SecretKey secretKey = new SecretKeySpec(this.getKey(), "AES"); Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); aes.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(IV)); return aes.doFinal(plainText); } catch (GeneralSecurityException ex) { throw new EJBException(ex); } } /* */ public byte[] decrypt(byte[] cipherText) throws GeneralSecurityException { try { SecretKey secretKey = new SecretKeySpec(this.getKey(), "AES"); Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); aes.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(IV)); byte[] plainText = aes.doFinal(cipherText); log.log(Level.FINE,"Decrypted to: {0} bytes", plainText.length); return plainText; } catch (Exception ex) { throw new GeneralSecurityException(ex); } } /* */ public byte[] encryptString(String plainText) { log.log(Level.FINE,"Encrypting: {0}", plainText); ByteArrayOutputStream buf = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(buf); try { // First write a timestamp long now = System.currentTimeMillis(); out.writeLong(now); out.writeUTF(plainText); out.close(); } catch (IOException ex) { // Should be impossible throw new RuntimeException(ex); } return this.encrypt(buf.toByteArray()); } /* */ public String decryptString(byte[] cipherText) throws GeneralSecurityException { return this.decryptString(cipherText, Long.MAX_VALUE); } /* */ public String decryptString(byte[] cipherText, long maxAgeMillis) throws GeneralSecurityException, ExpiredException { byte[] plainText = this.decrypt(cipherText); ByteArrayInputStream inBuf = new ByteArrayInputStream(plainText); DataInputStream in = new DataInputStream(inBuf); try { long time = in.readLong(); // First make sure token still good if ((System.currentTimeMillis() - time) > maxAgeMillis) throw new ExpiredException("Token expired"); String result = in.readUTF(); log.log(Level.FINE,"Decrypted to: {0}", result); return result; } catch (IOException ex) { // Should be impossible throw new RuntimeException(ex); } } /* */ public byte[] encryptList(List<String> parts) { ByteArrayOutputStream buf = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(buf); try { // First write a timestamp long now = System.currentTimeMillis(); out.writeLong(now); for (String part: parts) out.writeUTF(part); out.close(); } catch (IOException ex) { // Should be impossible throw new RuntimeException(ex); } return this.encrypt(buf.toByteArray()); } /* */ public List<String> decryptList(byte[] cipherText) throws GeneralSecurityException { return this.decryptList(cipherText, Long.MAX_VALUE); } /* */ public List<String> decryptList(byte[] cipherText, long maxAgeMillis) throws GeneralSecurityException, ExpiredException { byte[] plainText = this.decrypt(cipherText); ByteArrayInputStream inBuf = new ByteArrayInputStream(plainText); DataInputStream in = new DataInputStream(inBuf); try { long time = in.readLong(); // First make sure token still good if ((System.currentTimeMillis() - time) > maxAgeMillis) throw new ExpiredException("Token expired"); List<String> result = new ArrayList<String>(); while (in.available() > 0) result.add(in.readUTF()); return result; } catch (IOException ex) { // Should be impossible throw new RuntimeException(ex); } } }