Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encrypt credentials in memory #1349

Open
wants to merge 68 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
49c6586
Initialize Android KeyStore
avazirna Oct 9, 2023
09d9900
Retrieve key from Android KeyStore
avazirna Oct 10, 2023
c95b7c8
Refactor getSecretKey
avazirna Oct 10, 2023
676442b
Add method to check whether Android KeyStore is supported
avazirna Oct 10, 2023
93f33c8
Update encrypt and decrypt methods
avazirna Oct 10, 2023
cd6c546
Encrypt user credentials in memory
avazirna Oct 10, 2023
f20986c
Merge branch 'master' into encrypt-credentials-in-memory
avazirna Oct 10, 2023
7968312
Return plain username
avazirna Oct 11, 2023
c5d3f94
Lint
avazirna Oct 11, 2023
7b0a54d
Pass algorithm to cryptographic methods
avazirna Oct 12, 2023
a22ff5a
Refactor
avazirna Oct 13, 2023
3647d76
Add support to convert RSA keys
avazirna Oct 13, 2023
c485e5a
Set cryptographic transformation according to algorithm
avazirna Oct 13, 2023
b075ac9
Allow null IV for RSA ciphers
avazirna Oct 13, 2023
eff308b
Lint
avazirna Oct 13, 2023
99e70fd
Refactor
avazirna Oct 16, 2023
efdc30a
Refactor
avazirna Oct 16, 2023
448f074
Lint
avazirna Oct 16, 2023
0c39c5a
Update comments
avazirna Oct 16, 2023
5b66cc4
Set platform KeyStore name
avazirna Oct 16, 2023
8feef57
Lint
avazirna Nov 1, 2023
659f47e
Refactor
avazirna Nov 1, 2023
f6ac37d
Refactor secret key generation method
avazirna Nov 1, 2023
c67c1df
Refactor key pair generation method
avazirna Nov 1, 2023
e151f5f
Add unit tests for RSA
avazirna Nov 1, 2023
1069a52
Split credentials into plaintext and encrypted
avazirna Nov 23, 2023
87f6a93
Handle KeyStore exceptions by call
avazirna Nov 24, 2023
aae5ff2
Formatting
avazirna Nov 24, 2023
5f561fb
JavaDoc
avazirna Nov 24, 2023
92cef42
Add EncryptionKeyAndTransformation class
avazirna Nov 29, 2023
fcf749d
Remove KeyStore reference from CommCarePlatform
avazirna Nov 29, 2023
276c050
Add EncryptionKeyProvider service interface
avazirna Nov 30, 2023
0a32085
Add service loader utility class
avazirna Nov 30, 2023
0b39e88
Add dummy encryption key provider
avazirna Nov 30, 2023
03f0ac0
Acquire an encryption key provider
avazirna Nov 30, 2023
eb4b3ed
Refactor
avazirna Nov 30, 2023
409a6f5
JavaDoc updates
avazirna Nov 30, 2023
5fe6f00
Add service providers configuration files
avazirna Nov 30, 2023
20b2068
Lint
avazirna Dec 4, 2023
4c429d4
Refactor
avazirna Dec 5, 2023
dbb1983
Merge branch 'master' into encrypt-credentials-in-memory
avazirna Dec 6, 2023
0b771c7
Lint
avazirna Dec 7, 2023
db3d264
Lint
avazirna Dec 13, 2023
7c9f89d
Redirect InvalidParameterException to XPathException
avazirna Dec 13, 2023
fb70f9b
Rename key alias
avazirna Dec 13, 2023
acd23a9
Add key algorithms constants
avazirna Dec 14, 2023
41cb1f2
Comment
avazirna Dec 14, 2023
dd45f8d
Refactor EncryptionUtils to EncryptionHelper
avazirna Dec 20, 2023
19fc7d8
Revert "Add unit tests for RSA"
avazirna Jan 19, 2024
3767176
Remove RSA option for encryption with encoded string
avazirna Jan 19, 2024
5b8f281
Rmove BasicEncryptionKeyProvider
avazirna Jan 22, 2024
2f269e8
Add EncryptionKeyHelper class
avazirna Jan 22, 2024
df1575a
Refactor IEncryptionKeyProvider to support KeyStore Key generation only
avazirna Jan 22, 2024
f7ab55b
Update javadoc
avazirna Jan 22, 2024
10352e1
Refactor
avazirna Jan 22, 2024
c3b79b9
Add key generation in KeyStore when alias is not found
avazirna Jan 22, 2024
94b0062
Make EncryptionHelper stateless
avazirna Jan 22, 2024
f176952
Refactor
avazirna Jan 22, 2024
7a82c35
Add EncryptionKeyException to umbrella exceptions related to encrypti…
avazirna Jan 22, 2024
96f0b04
Check IV lenght to apply GCMParameterSpec
avazirna Jan 22, 2024
ffc0c96
Check if there are valid EncryptionKeyProvider implementations
avazirna Jan 22, 2024
58ffb32
Throw EncryptionKeyException when key generation fails
avazirna Jan 23, 2024
b2a8da9
Rename IEncrypitonKeyProvider to IKeyStoreEncryptionKeyProvider
avazirna Jan 23, 2024
8c7363a
Refactor
avazirna Jan 23, 2024
188e22b
Update javadocs
avazirna Jan 23, 2024
a2928bd
Refactor
avazirna Jan 23, 2024
bd512d6
Bubble up encryption key exceptions
avazirna Jan 23, 2024
4716bda
Throw exception when there is no keyStore facility
avazirna Jan 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions src/main/java/org/commcare/core/encryption/CryptUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.commcare.core.encryption;

import org.commcare.util.EncryptionKeyHelper;
import org.javarosa.core.io.StreamsUtil;

import java.io.ByteArrayInputStream;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.commcare.core.interfaces;

import org.commcare.util.EncryptionKeyHelper;

import java.io.IOException;
import java.io.InputStream;

Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 8 additions & 2 deletions src/main/java/org/commcare/core/network/ModernHttpRequester.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -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<ResponseBody> response) throws IOException {
public InputStream getResponseStream(Response<ResponseBody> response)
throws IOException, EncryptionKeyHelper.EncryptionKeyException {
InputStream inputStream = response.body().byteStream();
BitCache cache = BitCacheFactory.getCache(cacheDirSetup, getContentLength(response));
cache.initializeCache();
Expand All @@ -187,7 +192,8 @@ public InputStream getResponseStream(Response<ResponseBody> 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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -8,7 +10,7 @@
* @author ctsims
*/
public interface BitCache {
void initializeCache() throws IOException;
void initializeCache() throws IOException, EncryptionKeyHelper.EncryptionKeyException;

OutputStream getCacheStream() throws IOException;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/commcare/util/CommCarePlatform.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
163 changes: 163 additions & 0 deletions src/main/java/org/commcare/util/EncryptionHelper.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading