diff --git a/src/main/java/org/commcare/core/encryption/CryptUtil.java b/src/main/java/org/commcare/core/encryption/CryptUtil.java index 60657f9fb1..3514f69578 100755 --- a/src/main/java/org/commcare/core/encryption/CryptUtil.java +++ b/src/main/java/org/commcare/core/encryption/CryptUtil.java @@ -1,5 +1,6 @@ package org.commcare.core.encryption; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.io.StreamsUtil; import java.io.ByteArrayInputStream; @@ -8,6 +9,8 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -138,16 +141,23 @@ public static SecretKey generateSymmetricKey(byte[] prngSeed) { return null; } - public static SecretKey generateSemiRandomKey() { + // Generate random Secret key with a default key lenght of 256 bits + public static SecretKey generateRandomSecretKey() + throws EncryptionKeyHelper.EncryptionKeyException { + final int AES_DEFAULT_KEY_LENGTH = 256; + return generateRandomSecretKey(AES_DEFAULT_KEY_LENGTH); + } + + public static SecretKey generateRandomSecretKey(int keylength) + throws EncryptionKeyHelper.EncryptionKeyException { KeyGenerator generator; try { generator = KeyGenerator.getInstance("AES"); - generator.init(256, new SecureRandom()); + generator.init(keylength, new SecureRandom()); return generator.generateKey(); } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); + throw new EncryptionKeyHelper.EncryptionKeyException("Error encountered while generating random Key Pair", e); } - return null; } public static Cipher getPrivateKeyCipher(byte[] privateKey) @@ -176,4 +186,17 @@ private static Cipher getAesKeyCipher(byte[] aesKey, int mode) decrypter.init(mode, spec); return decrypter; } + + // For RSA + public static KeyPair generateRandomKeyPair(int keyLength) + throws EncryptionKeyHelper.EncryptionKeyException { + KeyPairGenerator generator; + try { + generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(keyLength, new SecureRandom()); + return generator.genKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new EncryptionKeyHelper.EncryptionKeyException("Error encountered while generating random Key Pair", e); + } + } } diff --git a/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java b/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java index a922464635..f4559316ac 100644 --- a/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java +++ b/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java @@ -1,5 +1,7 @@ package org.commcare.core.interfaces; +import org.commcare.util.EncryptionKeyHelper; + import java.io.IOException; import java.io.InputStream; @@ -36,4 +38,9 @@ public interface HttpResponseProcessor { * A issue occurred while processing the http request or response */ void handleIOException(IOException exception); + + /** + * Encryption key error occurred while processing the http response + */ + void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException mException); } diff --git a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java index 2309b441f0..9a687a92bc 100644 --- a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java +++ b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java @@ -1,8 +1,10 @@ package org.commcare.core.interfaces; +import org.commcare.util.EncryptionKeyHelper; + import java.io.IOException; import java.io.InputStream; public interface ResponseStreamAccessor { - InputStream getResponseStream() throws IOException; + InputStream getResponseStream() throws IOException, EncryptionKeyHelper.EncryptionKeyException; } diff --git a/src/main/java/org/commcare/core/network/ModernHttpRequester.java b/src/main/java/org/commcare/core/network/ModernHttpRequester.java index 3429596d00..613c048118 100644 --- a/src/main/java/org/commcare/core/network/ModernHttpRequester.java +++ b/src/main/java/org/commcare/core/network/ModernHttpRequester.java @@ -6,6 +6,7 @@ import org.commcare.core.interfaces.ResponseStreamAccessor; import org.commcare.core.network.bitcache.BitCache; import org.commcare.core.network.bitcache.BitCacheFactory; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.NetworkStatus; import org.javarosa.core.io.StreamsUtil; @@ -152,6 +153,9 @@ public static void processResponse(HttpResponseProcessor responseProcessor, } catch (IOException e) { responseProcessor.handleIOException(e); return; + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + responseProcessor.handleEncryptionKeyException(e); + return; } responseProcessor.processSuccess(responseCode, responseStream); } finally { @@ -172,7 +176,8 @@ public static void processResponse(HttpResponseProcessor responseProcessor, * @throws IOException if an io error happens while reading or writing to cache */ - public InputStream getResponseStream(Response response) throws IOException { + public InputStream getResponseStream(Response response) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { InputStream inputStream = response.body().byteStream(); BitCache cache = BitCacheFactory.getCache(cacheDirSetup, getContentLength(response)); cache.initializeCache(); @@ -187,7 +192,8 @@ public InputStream getResponseStream(Response response) throws IOE * @throws IOException if an io error happens while reading or writing to cache */ @Override - public InputStream getResponseStream() throws IOException { + public InputStream getResponseStream() + throws IOException, EncryptionKeyHelper.EncryptionKeyException { return getResponseStream(response); } diff --git a/src/main/java/org/commcare/core/network/bitcache/BitCache.java b/src/main/java/org/commcare/core/network/bitcache/BitCache.java index 8a5d9c0981..c133f8e358 100755 --- a/src/main/java/org/commcare/core/network/bitcache/BitCache.java +++ b/src/main/java/org/commcare/core/network/bitcache/BitCache.java @@ -1,5 +1,7 @@ package org.commcare.core.network.bitcache; +import org.commcare.util.EncryptionKeyHelper; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -8,7 +10,7 @@ * @author ctsims */ public interface BitCache { - void initializeCache() throws IOException; + void initializeCache() throws IOException, EncryptionKeyHelper.EncryptionKeyException; OutputStream getCacheStream() throws IOException; diff --git a/src/main/java/org/commcare/core/network/bitcache/FileBitCache.java b/src/main/java/org/commcare/core/network/bitcache/FileBitCache.java index 5284507cb7..e84ab8a6ca 100755 --- a/src/main/java/org/commcare/core/network/bitcache/FileBitCache.java +++ b/src/main/java/org/commcare/core/network/bitcache/FileBitCache.java @@ -1,6 +1,7 @@ package org.commcare.core.network.bitcache; import org.commcare.core.encryption.CryptUtil; +import org.commcare.util.EncryptionKeyHelper; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -33,12 +34,12 @@ protected FileBitCache(BitCacheFactory.CacheDirSetup cacheDirSetup) { } @Override - public void initializeCache() throws IOException { + public void initializeCache() throws IOException, EncryptionKeyHelper.EncryptionKeyException { File cacheLocation = cacheDirSetup.getCacheDir(); //generate temp file temp = File.createTempFile("commcare_pull_" + new Date().getTime(), "xml", cacheLocation); - key = CryptUtil.generateSemiRandomKey(); + key = CryptUtil.generateRandomSecretKey(); } @Override diff --git a/src/main/java/org/commcare/util/CommCarePlatform.java b/src/main/java/org/commcare/util/CommCarePlatform.java index fae9888858..48acab0906 100644 --- a/src/main/java/org/commcare/util/CommCarePlatform.java +++ b/src/main/java/org/commcare/util/CommCarePlatform.java @@ -39,6 +39,7 @@ public class CommCarePlatform { // TODO: We should make this unique using the parser to invalidate this ID or something public static final String APP_PROFILE_RESOURCE_ID = "commcare-application-profile"; + private int profile; private Profile cachedProfile; diff --git a/src/main/java/org/commcare/util/EncryptionHelper.java b/src/main/java/org/commcare/util/EncryptionHelper.java new file mode 100644 index 0000000000..7fd309a783 --- /dev/null +++ b/src/main/java/org/commcare/util/EncryptionHelper.java @@ -0,0 +1,163 @@ +package org.commcare.util; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; + +public class EncryptionHelper { + + public enum CryptographicOperation {Encryption, Decryption} + + /** + * Encrypts a message using a key stored in the platform KeyStore. The key is retrieved using + * its alias which is established during key generation. + * + * @param message a UTF-8 encoded message to be encrypted + * @param keyAlias alias of the Key stored in the KeyStore, depending on the algorithm, + * it can be a SecretKey (for AES) or PublicKey (for RSA) to be used to + * encrypt the message + * @return A base64 encoded payload containing the IV and AES or RSA encrypted ciphertext, + * which can be decoded by this utility's decrypt method and the same key + */ + public static String encryptWithKeyStore(String message, String keyAlias) + throws EncryptionException, EncryptionKeyHelper.EncryptionKeyException { + EncryptionKeyAndTransformation keyAndTransformation = + EncryptionKeyHelper.retrieveKeyFromKeyStore(keyAlias, CryptographicOperation.Encryption); + + return encrypt(message, keyAndTransformation); + } + + public static String encryptWithEncodedKey(String message, String key) + throws EncryptionException, EncryptionKeyHelper.EncryptionKeyException { + EncryptionKeyAndTransformation keyAndTransformation = + EncryptionKeyHelper.retrieveKeyFromEncodedKey(key); + + return encrypt(message, keyAndTransformation); + } + + /** + * Encrypts a message using the AES or RAS algorithms and produces a base64 encoded payload + * containing the ciphertext, and, when applicable, a random IV which was used to encrypt + * the input. + * + * @param message a UTF-8 encoded message to be encrypted + * @param keyAndTransform depending on the algorithm, a SecretKey or PublicKey, and + * cryptographic transformation to be used to encrypt the message + * @return A base64 encoded payload containing the IV and AES or RSA encrypted ciphertext, + * which can be decoded by this utility's decrypt method and the same key + */ + private static String encrypt(String message, EncryptionKeyAndTransformation keyAndTransform) + throws EncryptionException { + final int MIN_IV_LENGTH_BYTE = 1; + final int MAX_IV_LENGTH_BYTE = 255; + + try { + Cipher cipher = Cipher.getInstance(keyAndTransform.getTransformation()); + cipher.init(Cipher.ENCRYPT_MODE, keyAndTransform.getKey()); + byte[] encryptedMessage = cipher.doFinal(message.getBytes(Charset.forName("UTF-8"))); + byte[] iv = cipher.getIV(); + int ivSize = (iv == null ? 0 : iv.length); + if (ivSize == 0) { + iv = new byte[0]; + } else if (ivSize < MIN_IV_LENGTH_BYTE || ivSize > MAX_IV_LENGTH_BYTE) { + throw new EncryptionException("Initialization vector should be between " + + MIN_IV_LENGTH_BYTE + " and " + MAX_IV_LENGTH_BYTE + + " bytes long, but it is " + ivSize + " bytes"); + } + // The conversion of iv.length to byte takes the low 8 bits. To + // convert back, cast to int and mask with 0xFF. + byte[] ivPlusMessage = ByteBuffer.allocate(1 + ivSize + encryptedMessage.length) + .put((byte)ivSize) + .put(iv) + .put(encryptedMessage) + .array(); + return Base64.encode(ivPlusMessage); + } catch (Exception ex) { + throw new EncryptionException("Unknown error during encryption", ex); + } + } + + /** + * Decrypts a base64 payload containing an IV and AES or RSA encrypted ciphertext using a key + * stored in the platform KeyStore. The key is retrieved using its alias which is established + * during key generation. + * + * @param message a UTF-8 encoded message to be decrypted + * @param keyAlias key alias of the Key stored in the KeyStore, depending on the algorithm, + * it can be a SecretKey (for AES) or PrivateKey (for RSA) to be used to + * decrypt the message + * @return Decrypted message of the provided ciphertext, + */ + public static String decryptWithKeyStore(String message, String keyAlias) + throws EncryptionKeyHelper.EncryptionKeyException, EncryptionHelper.EncryptionException { + EncryptionKeyAndTransformation keyAndTransformation = + EncryptionKeyHelper.retrieveKeyFromKeyStore(keyAlias, CryptographicOperation.Decryption); + + return decrypt(message, keyAndTransformation); + } + + public static String decryptWithEncodedKey(String message, String key) + throws EncryptionException, EncryptionKeyHelper.EncryptionKeyException { + EncryptionKeyAndTransformation keyAndTransformation = + EncryptionKeyHelper.retrieveKeyFromEncodedKey(key); + + return decrypt(message, keyAndTransformation); + } + + /** + * Decrypts a base64 payload containing an IV and AES or RSA encrypted ciphertext using the + * provided key + * + * @param message a message to be decrypted + * @param keyAndTransform depending on the algorithm, a Secret key or Private key and its + * respective cryptographic transformation to be used for decryption + * @return Decrypted message for the given encrypted message + */ + private static String decrypt(String message, EncryptionKeyAndTransformation keyAndTransform) + throws EncryptionException { + final int TAG_LENGTH_BIT = 128; + + try { + byte[] messageBytes = Base64.decode(message); + ByteBuffer bb = ByteBuffer.wrap(messageBytes); + int ivLengthByte = bb.get() & 0xFF; + byte[] iv = new byte[ivLengthByte]; + bb.get(iv); + + byte[] cipherText = new byte[bb.remaining()]; + bb.get(cipherText); + + Cipher cipher = Cipher.getInstance(keyAndTransform.getTransformation()); + if (ivLengthByte > 0) { + cipher.init(Cipher.DECRYPT_MODE, keyAndTransform.getKey(), new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + } else { + cipher.init(Cipher.DECRYPT_MODE, keyAndTransform.getKey()); + } + byte[] plainText = cipher.doFinal(cipherText); + return new String(plainText, Charset.forName("UTF-8")); + } catch (NoSuchAlgorithmException | BadPaddingException | NoSuchPaddingException | + IllegalBlockSizeException | InvalidKeyException | Base64DecoderException | + InvalidAlgorithmParameterException e) { + throw new EncryptionException("Error encountered while decrypting the message", e); + } + } + + public static class EncryptionException extends Exception { + + public EncryptionException(String message) { + super(message); + } + + public EncryptionException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/org/commcare/util/EncryptionKeyAndTransformation.java b/src/main/java/org/commcare/util/EncryptionKeyAndTransformation.java new file mode 100644 index 0000000000..f8dd2d9b67 --- /dev/null +++ b/src/main/java/org/commcare/util/EncryptionKeyAndTransformation.java @@ -0,0 +1,26 @@ +package org.commcare.util; + +import java.security.Key; + +/** + * Utility class for holding an encryption key and transformation string pair + * + * @author dviggiano + */ +public class EncryptionKeyAndTransformation { + private Key key; + private String transformation; + + public EncryptionKeyAndTransformation(Key key, String transformation) { + this.key = key; + this.transformation = transformation; + } + + public Key getKey() { + return key; + } + + public String getTransformation() { + return transformation; + } +} \ No newline at end of file diff --git a/src/main/java/org/commcare/util/EncryptionKeyHelper.java b/src/main/java/org/commcare/util/EncryptionKeyHelper.java new file mode 100644 index 0000000000..829ab1ca17 --- /dev/null +++ b/src/main/java/org/commcare/util/EncryptionKeyHelper.java @@ -0,0 +1,120 @@ +package org.commcare.util; + +import java.io.IOException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; + +import javax.crypto.spec.SecretKeySpec; + +public class EncryptionKeyHelper { + + // these key algorithm constants are to be used only outside of any Keystore scope + public static final String CC_KEY_ALGORITHM_AES = "AES"; + public static final String CC_KEY_ALGORITHM_RSA = "RSA"; + public static final String CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS = "cc-in-memory-encryption-key-alias"; + + private static final IKeyStoreEncryptionKeyProvider keyStoreEncryptionKeyProvider = KeyStoreEncryptionKeyServiceProvider.getInstance().serviceImpl(); + + private static KeyStore keystoreSingleton = null; + + private static KeyStore getKeyStore() throws EncryptionKeyException { + if (keystoreSingleton == null) { + try { + keystoreSingleton = KeyStore.getInstance(keyStoreEncryptionKeyProvider.getKeyStoreName()); + keystoreSingleton.load(null); + } catch (KeyStoreException | CertificateException | IOException | + NoSuchAlgorithmException e) { + throw new EncryptionKeyException("KeyStore failed to initialize", e); + } + } + return keystoreSingleton; + } + + /** + * Converts a Base64 encoded key into a SecretKey depending on the algorithm + * + * @param base64encodedKey key in String format + * @return Secret key to be used to encrypt/decrypt data + */ + public static EncryptionKeyAndTransformation retrieveKeyFromEncodedKey(String base64encodedKey) + throws EncryptionKeyException { + final int KEY_LENGTH_BIT = 256; + byte[] keyBytes; + try { + keyBytes = Base64.decode(base64encodedKey); + } catch (Base64DecoderException e) { + throw new EncryptionKeyException("Encryption key base 64 encoding is invalid", e); + } + + if (8 * keyBytes.length != KEY_LENGTH_BIT) { + throw new EncryptionKeyException("Key should be " + KEY_LENGTH_BIT + + " bits long, not " + 8 * keyBytes.length); + } + return new EncryptionKeyAndTransformation( + new SecretKeySpec(keyBytes, CC_KEY_ALGORITHM_AES), + "AES/GCM/NoPadding"); + } + + private static boolean isKeyStoreAvailable() { + return keyStoreEncryptionKeyProvider != null && + Security.getProvider(keyStoreEncryptionKeyProvider.getKeyStoreName()) != null; + } + + /** + * Returns an EncryptionKeyAndTransformation object that wraps a SecretKey, PrivateKey or + * PublicKey, depending on the cryptographic operation and the cryptographic transformation. + * This method generates a new key in case the alias doesn't exist. + * + * @param keyAlias alias of the key stored in the KeyStore + * @param cryptographicOperation Cryptographic operation where the key is to be used, relevant + * to the RSA algorithm + * @return EncryptionKeyAndTransformation to be used to encrypt/decrypt data + */ + public static EncryptionKeyAndTransformation retrieveKeyFromKeyStore(String keyAlias, + EncryptionHelper.CryptographicOperation cryptographicOperation) + throws EncryptionKeyException { + if (!isKeyStoreAvailable()) { + throw new EncryptionKeyException("No KeyStore facility available!"); + } + Key key; + try { + if (getKeyStore().containsAlias(keyAlias)) { + KeyStore.Entry keyEntry = getKeyStore().getEntry(keyAlias, null); + if (keyEntry instanceof KeyStore.PrivateKeyEntry) { + if (cryptographicOperation == EncryptionHelper.CryptographicOperation.Encryption) { + key = ((KeyStore.PrivateKeyEntry)keyEntry).getCertificate().getPublicKey(); + } else { + key = ((KeyStore.PrivateKeyEntry)keyEntry).getPrivateKey(); + } + } else { + key = ((KeyStore.SecretKeyEntry)keyEntry).getSecretKey(); + } + } else { + key = keyStoreEncryptionKeyProvider.generateCryptographicKeyInKeyStore(keyAlias, cryptographicOperation); + } + } catch (KeyStoreException| NoSuchAlgorithmException | UnrecoverableEntryException e) { + throw new EncryptionKeyException("Error retrieving key from KeyStore", e); + } + if (key != null) { + return new EncryptionKeyAndTransformation(key, keyStoreEncryptionKeyProvider.getTransformationString()); + } else { + throw new EncryptionKeyException("Key couldn't be found in the keyStore"); + } + } + + public static class EncryptionKeyException extends Exception { + + public EncryptionKeyException(String message) { + super(message); + } + + public EncryptionKeyException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java deleted file mode 100644 index cb317cb313..0000000000 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.commcare.util; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public class EncryptionUtils { - - /** - * Encrypts a message using the AES encryption and produces a base64 encoded payload containing the ciphertext, and a random IV which was used to encrypt the input. - * - * @param message a UTF-8 encoded message to be encrypted - * @param key A base64 encoded 256 bit symmetric key - * @return A base64 encoded payload containing the IV and AES encrypted ciphertext, which can be decoded by this utility's decrypt method and the same symmetric key - */ - public static String encrypt(String message, String key) throws EncryptionException { - final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; - final int MIN_IV_LENGTH_BYTE = 1; - final int MAX_IV_LENGTH_BYTE = 255; - SecretKey secret = getSecretKeySpec(key); - - try { - Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); - cipher.init(Cipher.ENCRYPT_MODE, secret); - byte[] encryptedMessage = cipher.doFinal(message.getBytes(Charset.forName("UTF-8"))); - byte[] iv = cipher.getIV(); - if (iv.length < MIN_IV_LENGTH_BYTE || iv.length > MAX_IV_LENGTH_BYTE) { - throw new EncryptionException("Initialization vector should be between " + - MIN_IV_LENGTH_BYTE + " and " + MAX_IV_LENGTH_BYTE + - " bytes long, but it is " + iv.length + " bytes"); - } - // The conversion of iv.length to byte takes the low 8 bits. To - // convert back, cast to int and mask with 0xFF. - byte[] ivPlusMessage = ByteBuffer.allocate(1 + iv.length + encryptedMessage.length) - .put((byte)iv.length) - .put(iv) - .put(encryptedMessage) - .array(); - return Base64.encode(ivPlusMessage); - } catch (Exception ex) { - throw new EncryptionException("Unknown error during encryption", ex); - } - } - - - private static SecretKey getSecretKeySpec(String key) throws EncryptionException { - final int KEY_LENGTH_BIT = 256; - byte[] keyBytes; - try { - keyBytes = Base64.decode(key); - } catch (Base64DecoderException e) { - throw new EncryptionException("Encryption key base 64 encoding is invalid", e); - } - if (8 * keyBytes.length != KEY_LENGTH_BIT) { - throw new EncryptionException("Key should be " + KEY_LENGTH_BIT + - " bits long, not " + 8 * keyBytes.length); - } - return new SecretKeySpec(keyBytes, "AES"); - } - - /** - * Decrypts a base64 payload containing an IV and AES encrypted ciphertext using the provided key - * - * @param message a message to be decrypted - * @param key key that should be used for decryption - * @return Decrypted message for the given AES encrypted message - */ - public static String decrypt(String message, String key) throws EncryptionException { - final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; - final int TAG_LENGTH_BIT = 128; - SecretKey secret = getSecretKeySpec(key); - - try { - byte[] messageBytes = Base64.decode(message); - ByteBuffer bb = ByteBuffer.wrap(messageBytes); - int iv_length_byte = bb.get() & 0xFF; - byte[] iv = new byte[iv_length_byte]; - bb.get(iv); - - byte[] cipherText = new byte[bb.remaining()]; - bb.get(cipherText); - - - Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); - cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); - byte[] plainText = cipher.doFinal(cipherText); - return new String(plainText, Charset.forName("UTF-8")); - } catch (NoSuchAlgorithmException | BadPaddingException | NoSuchPaddingException | - IllegalBlockSizeException | InvalidAlgorithmParameterException | InvalidKeyException | Base64DecoderException e) { - throw new EncryptionException("Error encountered while decrypting the message", e); - } - } - - public static class EncryptionException extends Exception { - - public EncryptionException(String message) { - super(message); - } - - public EncryptionException(String message, Throwable cause) { - super(message, cause); - } - } -} diff --git a/src/main/java/org/commcare/util/IKeyStoreEncryptionKeyProvider.java b/src/main/java/org/commcare/util/IKeyStoreEncryptionKeyProvider.java new file mode 100644 index 0000000000..727eed2c51 --- /dev/null +++ b/src/main/java/org/commcare/util/IKeyStoreEncryptionKeyProvider.java @@ -0,0 +1,20 @@ +package org.commcare.util; + +import java.security.Key; + +/** + * Service interface for Encryption Key providers for KeyStores + * + * @author avazirna + */ + +public interface IKeyStoreEncryptionKeyProvider { + + Key generateCryptographicKeyInKeyStore(String keyAlias, + EncryptionHelper.CryptographicOperation cryptographicOperation) + throws EncryptionKeyHelper.EncryptionKeyException; + + String getTransformationString(); + + String getKeyStoreName(); +} diff --git a/src/main/java/org/commcare/util/KeyStoreEncryptionKeyServiceProvider.java b/src/main/java/org/commcare/util/KeyStoreEncryptionKeyServiceProvider.java new file mode 100644 index 0000000000..c7847ec39d --- /dev/null +++ b/src/main/java/org/commcare/util/KeyStoreEncryptionKeyServiceProvider.java @@ -0,0 +1,36 @@ +package org.commcare.util; + +import java.util.ServiceLoader; + +/** + * Utility class responsible for finding implementations of IEncryptionKeyProvider during runtime + * and loading them in memory + * + * @author avazirna + */ + +public class KeyStoreEncryptionKeyServiceProvider { + private static KeyStoreEncryptionKeyServiceProvider serviceProvider; + private ServiceLoader loader; + + private KeyStoreEncryptionKeyServiceProvider() { + loader = ServiceLoader.load(IKeyStoreEncryptionKeyProvider.class); + } + + public static KeyStoreEncryptionKeyServiceProvider getInstance() { + if (serviceProvider == null) { + serviceProvider = new KeyStoreEncryptionKeyServiceProvider(); + } + return serviceProvider; + } + + public IKeyStoreEncryptionKeyProvider serviceImpl() { + IKeyStoreEncryptionKeyProvider service = null; + if (loader.iterator().hasNext()) { + service = loader.iterator().next(); + } + + return service; + } +} + diff --git a/src/main/java/org/commcare/util/LogTypes.java b/src/main/java/org/commcare/util/LogTypes.java index b3a2580b29..2cce7b1e99 100644 --- a/src/main/java/org/commcare/util/LogTypes.java +++ b/src/main/java/org/commcare/util/LogTypes.java @@ -114,4 +114,8 @@ public class LogTypes { */ public static final String TYPE_FCM = "fcm"; + /** + * Errors while generating/handling encryption keys + */ + public static final String TYPE_ERROR_ENCRYPTION_KEY = "encryption-key"; } diff --git a/src/main/java/org/javarosa/core/model/User.java b/src/main/java/org/javarosa/core/model/User.java index b0ed5770fd..82c32774d2 100644 --- a/src/main/java/org/javarosa/core/model/User.java +++ b/src/main/java/org/javarosa/core/model/User.java @@ -1,5 +1,7 @@ package org.javarosa.core.model; +import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.util.restorable.Restorable; @@ -14,8 +16,14 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; import java.util.Hashtable; +import static org.commcare.util.EncryptionKeyHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS; + /** * Peristable object representing a CommCare mobile user. * @@ -36,7 +44,8 @@ public class User implements Persistable, Restorable, IMetaData { public static final String META_SYNC_TOKEN = "synctoken"; public int recordId = -1; //record id on device - private String username; + private String plaintextUsername; + private String encryptedUsername; private String passwordHash; private String uniqueId; //globally-unique id @@ -46,6 +55,11 @@ public class User implements Persistable, Restorable, IMetaData { public Hashtable properties = new Hashtable<>(); + // plaintextCachedPwd and encryptedCachedPwd are used to store the password in memory, should + // not to be persisted. For aspects related to persisting the password, refer to passwordHash + private String plaintextCachedPwd; + private String encryptedCachedPwd; + public User() { setUserType(STANDARD); } @@ -55,7 +69,7 @@ public User(String name, String passw, String uniqueID) { } public User(String name, String passw, String uniqueID, String userType) { - username = name; + setUsername(name); passwordHash = passw; uniqueId = uniqueID; setUserType(userType); @@ -65,7 +79,7 @@ public User(String name, String passw, String uniqueID, String userType) { // fetch the value for the default user and password from the RMS @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { - this.username = ExtUtil.readString(in); + setUsername(ExtUtil.readString(in)); this.passwordHash = ExtUtil.readString(in); this.recordId = ExtUtil.readInt(in); this.uniqueId = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); @@ -77,7 +91,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep @Override public void writeExternal(DataOutputStream out) throws IOException { - ExtUtil.writeString(out, username); + ExtUtil.writeString(out, getUsername()); ExtUtil.writeString(out, passwordHash); ExtUtil.writeNumeric(out, recordId); ExtUtil.writeString(out, ExtUtil.emptyIfNull(uniqueId)); @@ -88,7 +102,17 @@ public void writeExternal(DataOutputStream out) throws IOException { } public String getUsername() { - return username; + if (this.plaintextUsername != null) { + return this.plaintextUsername; + } + + try { + return EncryptionHelper.decryptWithKeyStore(this.encryptedUsername, CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + throw new RuntimeException("Error encountered while retrieving key from keyStore", e); + } catch (EncryptionHelper.EncryptionException e) { + throw new RuntimeException("Error encountered while decrypting the username", e); + } } public String getPasswordHash() { @@ -118,7 +142,16 @@ public void setUserType(String userType) { } public void setUsername(String username) { - this.username = username; + try { + this.encryptedUsername = + EncryptionHelper.encryptWithKeyStore(username, CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); + // set this to null in case it was set in a previous call + this.plaintextUsername = null; + } catch (EncryptionKeyHelper.EncryptionKeyException + | EncryptionHelper.EncryptionException e) { + e.printStackTrace(); + this.plaintextUsername = username; + } } public void setPassword(String passwordHash) { @@ -163,9 +196,9 @@ public void templateData(FormInstance dm, TreeReference parentRef) { public Object getMetaData(String fieldName) { if (META_UID.equals(fieldName)) { return uniqueId; - } else if(META_USERNAME.equals(fieldName)) { - return username; - } else if(META_ID.equals(fieldName)) { + } else if (META_USERNAME.equals(fieldName)) { + return getUsername(); + } else if (META_ID.equals(fieldName)) { return recordId; } else if (META_WRAPPED_KEY.equals(fieldName)) { return wrappedKey; @@ -178,16 +211,34 @@ public Object getMetaData(String fieldName) { // TODO: Add META_WRAPPED_KEY back in? @Override public String[] getMetaDataFields() { - return new String[] {META_UID, META_USERNAME, META_ID, META_SYNC_TOKEN}; + return new String[]{META_UID, META_USERNAME, META_ID, META_SYNC_TOKEN}; } - //Don't ever save! - private String cachedPwd; public void setCachedPwd(String password) { - this.cachedPwd = password; + try { + this.encryptedCachedPwd = + EncryptionHelper.encryptWithKeyStore(password, CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); + // set this to null in case it was set in a previous call + this.plaintextCachedPwd = null; + } catch (EncryptionKeyHelper.EncryptionKeyException + | EncryptionHelper.EncryptionException e) { + e.printStackTrace(); + this.plaintextCachedPwd = password; + } } + public String getCachedPwd() { - return this.cachedPwd; + if (this.plaintextCachedPwd != null) { + return this.plaintextCachedPwd; + } + + try { + return EncryptionHelper.decryptWithKeyStore(this.encryptedCachedPwd, CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + throw new RuntimeException("Error encountered while retrieving key from keyStore ", e); + } catch (EncryptionHelper.EncryptionException e) { + throw new RuntimeException("Error encountered while decrypting the password ", e); + } } public String getLastSyncToken() { diff --git a/src/main/java/org/javarosa/core/model/utils/DateUtils.java b/src/main/java/org/javarosa/core/model/utils/DateUtils.java index 5d6337e0b6..62ee1edb23 100755 --- a/src/main/java/org/javarosa/core/model/utils/DateUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/DateUtils.java @@ -947,7 +947,6 @@ public static boolean stringContains(String string, String substring) { return string.contains(substring); } - // TODO: Move this method to DateUtils public static String convertTimeInMsToISO8601(long ms) { if (ms == 0) { return ""; diff --git a/src/main/java/org/javarosa/xpath/expr/XPathDecryptStringFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathDecryptStringFunc.java index 0dd8cd47dd..d0478faf06 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathDecryptStringFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathDecryptStringFunc.java @@ -1,18 +1,20 @@ package org.javarosa.xpath.expr; -import static org.commcare.util.EncryptionUtils.decrypt; -import static org.commcare.util.EncryptionUtils.encrypt; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.DataInstance; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.parser.XPathSyntaxException; +import static org.commcare.util.EncryptionKeyHelper.CC_KEY_ALGORITHM_AES; + public class XPathDecryptStringFunc extends XPathFuncExpr { public static final String NAME = "decrypt-string"; private static final int EXPECTED_ARG_COUNT = 3; + private EncryptionHelper encryptionHelper = new EncryptionHelper(); public XPathDecryptStringFunc() { name = NAME; @@ -35,19 +37,21 @@ public Object evalBody(DataInstance model, EvaluationContext evalContext, Object * @param o2 the key used for encryption * @param o3 the encryption algorithm to use */ - private static String decryptString(Object o1, Object o2, Object o3) { + private String decryptString(Object o1, Object o2, Object o3) { String message = FunctionUtils.toString(o1); String key = FunctionUtils.toString(o2); String algorithm = FunctionUtils.toString(o3); - if (!algorithm.equals("AES")) { + + if (!algorithm.equals(CC_KEY_ALGORITHM_AES)) { throw new XPathException("Unknown algorithm \"" + algorithm + "\" for " + NAME); } try { - return decrypt(message, key); - } catch (EncryptionUtils.EncryptionException e) { + return encryptionHelper.decryptWithEncodedKey(message, key); + } catch (EncryptionHelper.EncryptionException | + EncryptionKeyHelper.EncryptionKeyException e) { throw new XPathException(e); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathEncryptStringFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathEncryptStringFunc.java index 54e3447e81..bb53c8c5b0 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathEncryptStringFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathEncryptStringFunc.java @@ -1,9 +1,10 @@ package org.javarosa.xpath.expr; -import static org.commcare.util.EncryptionUtils.encrypt; +import static org.commcare.util.EncryptionKeyHelper.CC_KEY_ALGORITHM_AES; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.DataInstance; import org.javarosa.xpath.XPathException; @@ -13,6 +14,7 @@ public class XPathEncryptStringFunc extends XPathFuncExpr { public static final String NAME = "encrypt-string"; private static final int EXPECTED_ARG_COUNT = 3; + private EncryptionHelper encryptionHelper = new EncryptionHelper(); public XPathEncryptStringFunc() { name = NAME; @@ -35,20 +37,21 @@ public Object evalBody(DataInstance model, EvaluationContext evalContext, Object * @param o2 the key used for encryption * @param o3 the encryption algorithm to use */ - private static String encryptString(Object o1, Object o2, Object o3) { + private String encryptString(Object o1, Object o2, Object o3) { String message = FunctionUtils.toString(o1); String key = FunctionUtils.toString(o2); String algorithm = FunctionUtils.toString(o3); - if (!algorithm.equals("AES")) { + if (!algorithm.equals(CC_KEY_ALGORITHM_AES)) { throw new XPathException("Unknown algorithm \"" + algorithm + "\" for " + NAME); } try { - return encrypt(message, key); - } catch (EncryptionUtils.EncryptionException e) { + return encryptionHelper.encryptWithEncodedKey(message, key); + } catch (EncryptionHelper.EncryptionException | + EncryptionKeyHelper.EncryptionKeyException e) { throw new XPathException(e); } } -} +} \ No newline at end of file diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 24b69d2745..697a98ae4d 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.fail; -import org.commcare.util.EncryptionUtils; +import org.commcare.core.encryption.CryptUtil; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.condition.IFunctionHandler; import org.javarosa.core.model.data.IAnswerData; @@ -32,12 +32,10 @@ import org.junit.Test; import java.io.UnsupportedEncodingException; -import java.security.SecureRandom; import java.util.Base64; import java.util.Date; import java.util.Vector; -import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; public class XPathEvalTest { @@ -724,7 +722,7 @@ public void testStringOutputs() throws XPathSyntaxException { public void testStringOutput(String xPathInput) throws XPathSyntaxException { XPathExpression expr = XPathParseTool.parseXPath(xPathInput); - Assert.assertEquals(xPathInput,expr.toPrettyString()); + Assert.assertEquals(xPathInput, expr.toPrettyString()); } @@ -766,19 +764,12 @@ public Object eval(Object[] args, EvaluationContext ec) { testEval("now()", null, ec, "pass"); } - // Utility methods for string encryption. - private SecretKey generateSecretKey(int keyLength) throws Exception { - KeyGenerator keyGen = KeyGenerator.getInstance("AES"); - keyGen.init(keyLength, new SecureRandom()); - return keyGen.generateKey(); - } - public void encryptAndCompare(EvaluationContext ec, String algorithm, int keyLength, String message, Exception expectedException) throws UnsupportedEncodingException { SecretKey secretKey = null; try { - secretKey = generateSecretKey(keyLength); + secretKey = CryptUtil.generateRandomSecretKey(keyLength); } catch(Exception ex) { fail("Unexpected exception generating secret key"); } @@ -800,11 +791,11 @@ public void encryptAndCompare(EvaluationContext ec, String algorithm, String decryptedMessage = FunctionUtils.toString(decryptedObject); if (!message.equals(decryptedMessage)) { fail("Expected decrypted message " + message + ", got " + - decryptedMessage); + decryptedMessage); } - } catch(Exception ex) { + } catch (Exception ex) { assertExceptionExpected(expectedException != null, - expectedException, ex); + expectedException, ex); return; } }