From 77934f1332d2880cd650673a2e5c1a743117a7c5 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 10 Oct 2023 01:46:21 +0200 Subject: [PATCH 01/42] Generate keys for KeyStore --- .../org/commcare/utils/EncryptionUtils.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/app/src/org/commcare/utils/EncryptionUtils.java b/app/src/org/commcare/utils/EncryptionUtils.java index dc620c308e..b9b9c3471e 100644 --- a/app/src/org/commcare/utils/EncryptionUtils.java +++ b/app/src/org/commcare/utils/EncryptionUtils.java @@ -1,9 +1,25 @@ package org.commcare.utils; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import org.commcare.CommCareApplication; import org.commcare.util.Base64; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.GregorianCalendar; + +import javax.crypto.KeyGenerator; +import javax.security.auth.x500.X500Principal; + +import static org.commcare.util.EncryptionUtils.ANDROID_KEYSTORE_PROVIDER_NAME; /** * Utility class for encrypting submissions during the SaveToDiskTask. @@ -22,4 +38,47 @@ public static String getMD5HashAsString(String plainText) { return ""; } } + + public static void generateKeyForKeyStore(String keyAlias) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + KeyGenerator keyGenerator = KeyGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER_NAME); + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build(); + keyGenerator.init(keyGenParameterSpec); + keyGenerator.generateKey(); + } + else { + KeyPairGenerator keyGenerator = KeyPairGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE_PROVIDER_NAME); + GregorianCalendar start = new GregorianCalendar(); + GregorianCalendar end = new GregorianCalendar(); + end.add(GregorianCalendar.YEAR, 1); + + KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) + // Key alias to be used to retrieve it from the KeyStore + .setAlias(keyAlias) + // The subject used for the self-signed certificate of the generated pair + .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) + // The serial number used for the self-signed certificate of the + // generated pair + .setSerialNumber(BigInteger.valueOf(1337)) + // Date range of validity for the generated pair + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + + keyGenerator.initialize(keySpec); + keyGenerator.generateKeyPair(); + } + + } catch (NoSuchAlgorithmException | NoSuchProviderException | + InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } } From b6cf455733b7dfc033d825964ca1092cf66334fe Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 10 Oct 2023 02:14:10 +0200 Subject: [PATCH 02/42] Refactor --- app/src/org/commcare/android/nfc/NfcManager.java | 4 ++-- app/src/org/commcare/utils/EncryptionUtils.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index ca049329bb..611c012611 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -60,7 +60,7 @@ public String decryptValue(String message) throws EncryptionUtils.EncryptionExce if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = EncryptionUtils.decrypt(message, encryptionKey); + message = EncryptionUtils.decrypt(message, encryptionKey, true); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -97,7 +97,7 @@ public String tagAndEncryptPayload(String message) throws EncryptionUtils.Encryp } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = EncryptionUtils.encrypt(payload, encryptionKey); + payload = EncryptionUtils.encrypt(payload, encryptionKey, true); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); diff --git a/app/src/org/commcare/utils/EncryptionUtils.java b/app/src/org/commcare/utils/EncryptionUtils.java index b9b9c3471e..ff7e57ca84 100644 --- a/app/src/org/commcare/utils/EncryptionUtils.java +++ b/app/src/org/commcare/utils/EncryptionUtils.java @@ -39,7 +39,8 @@ public static String getMD5HashAsString(String plainText) { } } - public static void generateKeyForKeyStore(String keyAlias) { + // Generates a cryptrographic key and adds it to the Android KeyStore + public static void generateCryptographicKeyForKeyStore(String keyAlias) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyGenerator keyGenerator = KeyGenerator From c137fabe7475eb73044e295f8c03f6ee56459565 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 10 Oct 2023 13:06:50 +0200 Subject: [PATCH 03/42] Generate key during app initialization --- app/src/org/commcare/CommCareApplication.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index abea186b68..a6add53d14 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -140,6 +140,9 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; +import static org.commcare.util.EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS; +import static org.commcare.utils.EncryptionUtils.generateCryptographicKeyForKeyStore; + public class CommCareApplication extends MultiDexApplication { private static final String TAG = CommCareApplication.class.getSimpleName(); @@ -249,6 +252,8 @@ public void onCreate() { GraphUtil.setLabelCharacterLimit(getResources().getInteger(R.integer.graph_label_char_limit)); FirebaseMessagingUtil.verifyToken(); + + generateCryptographicKeyForKeyStore(USER_CREDENTIALS_KEY_ALIAS); } protected void attachISRGCert() { From 8fdcbf600ed36daa3808106b4064215e9bf6cc4e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 10 Oct 2023 15:10:38 +0200 Subject: [PATCH 04/42] Check is Android KeyStore is supported --- .../org/commcare/utils/EncryptionUtils.java | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/app/src/org/commcare/utils/EncryptionUtils.java b/app/src/org/commcare/utils/EncryptionUtils.java index ff7e57ca84..5f44481cea 100644 --- a/app/src/org/commcare/utils/EncryptionUtils.java +++ b/app/src/org/commcare/utils/EncryptionUtils.java @@ -20,6 +20,7 @@ import javax.security.auth.x500.X500Principal; import static org.commcare.util.EncryptionUtils.ANDROID_KEYSTORE_PROVIDER_NAME; +import static org.commcare.util.EncryptionUtils.isAndroidKeyStoreSupported; /** * Utility class for encrypting submissions during the SaveToDiskTask. @@ -41,45 +42,46 @@ public static String getMD5HashAsString(String plainText) { // Generates a cryptrographic key and adds it to the Android KeyStore public static void generateCryptographicKeyForKeyStore(String keyAlias) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - KeyGenerator keyGenerator = KeyGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER_NAME); - KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build(); - keyGenerator.init(keyGenParameterSpec); - keyGenerator.generateKey(); - } - else { - KeyPairGenerator keyGenerator = KeyPairGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE_PROVIDER_NAME); - GregorianCalendar start = new GregorianCalendar(); - GregorianCalendar end = new GregorianCalendar(); - end.add(GregorianCalendar.YEAR, 1); + if (isAndroidKeyStoreSupported()) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + KeyGenerator keyGenerator = KeyGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER_NAME); + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build(); + keyGenerator.init(keyGenParameterSpec); + keyGenerator.generateKey(); + } else { + KeyPairGenerator keyGenerator = KeyPairGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE_PROVIDER_NAME); + GregorianCalendar start = new GregorianCalendar(); + GregorianCalendar end = new GregorianCalendar(); + end.add(GregorianCalendar.YEAR, 1); - KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) - // Key alias to be used to retrieve it from the KeyStore - .setAlias(keyAlias) - // The subject used for the self-signed certificate of the generated pair - .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) - // The serial number used for the self-signed certificate of the - // generated pair - .setSerialNumber(BigInteger.valueOf(1337)) - // Date range of validity for the generated pair - .setStartDate(start.getTime()) - .setEndDate(end.getTime()) - .build(); + KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) + // Key alias to be used to retrieve it from the KeyStore + .setAlias(keyAlias) + // The subject used for the self-signed certificate of the generated pair + .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) + // The serial number used for the self-signed certificate of the + // generated pair + .setSerialNumber(BigInteger.valueOf(1337)) + // Date range of validity for the generated pair + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); - keyGenerator.initialize(keySpec); - keyGenerator.generateKeyPair(); - } + keyGenerator.initialize(keySpec); + keyGenerator.generateKeyPair(); + } - } catch (NoSuchAlgorithmException | NoSuchProviderException | - InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); + } catch (NoSuchAlgorithmException | NoSuchProviderException | + InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } } } } From 9722643b7b345afa89af4b381064ee561dd8e8df Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 13 Oct 2023 15:27:39 +0200 Subject: [PATCH 05/42] Refactor --- app/src/org/commcare/android/nfc/NfcManager.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index 611c012611..f0eb07e48d 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -1,11 +1,9 @@ package org.commcare.android.nfc; -import android.annotation.TargetApi; import android.app.PendingIntent; import android.content.Context; import android.content.IntentFilter; import android.nfc.NfcAdapter; -import android.os.Build; import org.apache.commons.lang3.StringUtils; import org.commcare.util.EncryptionUtils; @@ -60,7 +58,7 @@ public String decryptValue(String message) throws EncryptionUtils.EncryptionExce if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = EncryptionUtils.decrypt(message, encryptionKey, true); + message = EncryptionUtils.decryptUsingBase64EncodedKey("AES", message, encryptionKey); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -97,7 +95,7 @@ public String tagAndEncryptPayload(String message) throws EncryptionUtils.Encryp } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = EncryptionUtils.encrypt(payload, encryptionKey, true); + payload = EncryptionUtils.encryptUsingBase64EncodedKey("AES", payload, encryptionKey); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); From 0996c07ae7a0890a27728fa9ceb188a455d92c4e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Oct 2023 16:28:27 +0200 Subject: [PATCH 06/42] Refactor --- app/src/org/commcare/utils/EncryptionUtils.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/org/commcare/utils/EncryptionUtils.java b/app/src/org/commcare/utils/EncryptionUtils.java index 5f44481cea..fed25aeb0a 100644 --- a/app/src/org/commcare/utils/EncryptionUtils.java +++ b/app/src/org/commcare/utils/EncryptionUtils.java @@ -19,8 +19,9 @@ import javax.crypto.KeyGenerator; import javax.security.auth.x500.X500Principal; -import static org.commcare.util.EncryptionUtils.ANDROID_KEYSTORE_PROVIDER_NAME; -import static org.commcare.util.EncryptionUtils.isAndroidKeyStoreSupported; +import static org.commcare.util.CommCarePlatform.getPlatformKeyStoreName; +import static org.commcare.util.EncryptionUtils.isPlatformKeyStoreAvailable; + /** * Utility class for encrypting submissions during the SaveToDiskTask. @@ -42,11 +43,11 @@ public static String getMD5HashAsString(String plainText) { // Generates a cryptrographic key and adds it to the Android KeyStore public static void generateCryptographicKeyForKeyStore(String keyAlias) { - if (isAndroidKeyStoreSupported()) { + if (isPlatformKeyStoreAvailable()) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyGenerator keyGenerator = KeyGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER_NAME); + .getInstance(KeyProperties.KEY_ALGORITHM_AES, getPlatformKeyStoreName()); KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) @@ -55,8 +56,12 @@ public static void generateCryptographicKeyForKeyStore(String keyAlias) { keyGenerator.init(keyGenParameterSpec); keyGenerator.generateKey(); } else { + // Because KeyGenParameterSpec was introduced in Android SDK 23, prior versions + // need to resource to KeyPairGenerator which only generates asymmetric keys, + // hence the need to switch to a correspondent algorithm as well, RSA + // TODO: Add link to StackOverflow page KeyPairGenerator keyGenerator = KeyPairGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE_PROVIDER_NAME); + .getInstance(KeyProperties.KEY_ALGORITHM_RSA, getPlatformKeyStoreName()); GregorianCalendar start = new GregorianCalendar(); GregorianCalendar end = new GregorianCalendar(); end.add(GregorianCalendar.YEAR, 1); From f1201a9aaad06e3c8d43753e77e0e788b07a80f2 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 17 Oct 2023 00:07:13 +0200 Subject: [PATCH 07/42] Set Android key store name --- app/src/org/commcare/utils/AndroidCommCarePlatform.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/org/commcare/utils/AndroidCommCarePlatform.java b/app/src/org/commcare/utils/AndroidCommCarePlatform.java index a1beba2039..4383fe084c 100644 --- a/app/src/org/commcare/utils/AndroidCommCarePlatform.java +++ b/app/src/org/commcare/utils/AndroidCommCarePlatform.java @@ -37,6 +37,10 @@ public class AndroidCommCarePlatform extends CommCarePlatform { private final CommCareApp app; private String mUpdateInfoFormXmlns; + { + platformKeyStoreName = "AndroidKeyStore"; + } + public AndroidCommCarePlatform(int majorVersion, int minorVersion, int minimalVersion, CommCareApp app) { super(majorVersion, minorVersion, minimalVersion); xmlnstable = new Hashtable<>(); From daf20a300ebb268ac1c53bca5e904270f5700aae Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 1 Nov 2023 13:05:41 +0200 Subject: [PATCH 08/42] Refactor --- app/src/org/commcare/android/nfc/NfcManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index f0eb07e48d..a16e694574 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -58,7 +58,7 @@ public String decryptValue(String message) throws EncryptionUtils.EncryptionExce if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = EncryptionUtils.decryptUsingBase64EncodedKey("AES", message, encryptionKey); + message = EncryptionUtils.decryptWithBase64EncodedKey("AES", message, encryptionKey); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -95,7 +95,7 @@ public String tagAndEncryptPayload(String message) throws EncryptionUtils.Encryp } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = EncryptionUtils.encryptUsingBase64EncodedKey("AES", payload, encryptionKey); + payload = EncryptionUtils.encryptWithBase64EncodedKey("AES", payload, encryptionKey); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); From 9c54fe2dd2620b916b9b70ca92e03d4c28fb6669 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 1 Nov 2023 15:24:48 +0200 Subject: [PATCH 09/42] Refactor secret key generation method --- app/src/org/commcare/models/database/user/DemoUserBuilder.java | 2 +- app/src/org/commcare/tasks/DataPullTask.java | 2 +- app/src/org/commcare/utils/TemplatePrinterUtils.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/org/commcare/models/database/user/DemoUserBuilder.java b/app/src/org/commcare/models/database/user/DemoUserBuilder.java index 56e0304d8f..415de2f964 100644 --- a/app/src/org/commcare/models/database/user/DemoUserBuilder.java +++ b/app/src/org/commcare/models/database/user/DemoUserBuilder.java @@ -65,7 +65,7 @@ private void createAndWriteKeyRecordAndUser() { int userCount = keyRecordDB.getIDsForValue(UserKeyRecord.META_USERNAME, username).size(); if (userCount == 0) { - SecretKey secretKey = CryptUtil.generateSemiRandomKey(); + SecretKey secretKey = CryptUtil.generateRandomSecretKey(); if (secretKey == null) { throw new RuntimeException("Error setting up user's encrypted storage"); } diff --git a/app/src/org/commcare/tasks/DataPullTask.java b/app/src/org/commcare/tasks/DataPullTask.java index 6dbe8c1e22..180cb5a04c 100644 --- a/app/src/org/commcare/tasks/DataPullTask.java +++ b/app/src/org/commcare/tasks/DataPullTask.java @@ -223,7 +223,7 @@ private byte[] getEncryptionKey() { private void initUKRForLogin() { if (blockRemoteKeyManagement || shouldGenerateFirstKey()) { - SecretKey newKey = CryptUtil.generateSemiRandomKey(); + SecretKey newKey = CryptUtil.generateRandomSecretKey(); if (newKey == null) { return; } diff --git a/app/src/org/commcare/utils/TemplatePrinterUtils.java b/app/src/org/commcare/utils/TemplatePrinterUtils.java index 910b5b06d2..cf29bee5bf 100755 --- a/app/src/org/commcare/utils/TemplatePrinterUtils.java +++ b/app/src/org/commcare/utils/TemplatePrinterUtils.java @@ -38,7 +38,7 @@ public abstract class TemplatePrinterUtils { private static final String FORMAT_REGEX_WITH_DELIMITER = "((?<=%2$s)|(?=%1$s))"; - private static final SecretKey KEY = CryptUtil.generateSemiRandomKey(); + private static final SecretKey KEY = CryptUtil.generateRandomSecretKey(); /** * Concatenate all Strings in a String array to one String. From d149226532aba2cc11237ded528f4d9408265b3b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 1 Nov 2023 15:25:24 +0200 Subject: [PATCH 10/42] Refactor key pair generation method --- .../global/models/AndroidSharedKeyRecord.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java b/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java index ac9c628e78..4e62fbc811 100755 --- a/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java +++ b/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java @@ -14,9 +14,6 @@ import java.security.GeneralSecurityException; import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; /** * This is a record of a key that CommCare ODK has shared with another app @@ -52,18 +49,10 @@ public AndroidSharedKeyRecord(String keyId, byte[] privateKey, byte[] publicKey) } public static AndroidSharedKeyRecord generateNewSharingKey() { - try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); - generator.initialize(512, new SecureRandom()); - KeyPair pair = generator.genKeyPair(); - byte[] encodedPrivate = pair.getPrivate().getEncoded(); - String privateEncoding = pair.getPrivate().getFormat(); - byte[] encodedPublic = pair.getPublic().getEncoded(); - String publicencoding = pair.getPublic().getFormat(); - return new AndroidSharedKeyRecord(PropertyUtils.genUUID(), pair.getPrivate().getEncoded(), pair.getPublic().getEncoded()); - } catch (NoSuchAlgorithmException nsae) { - return null; - } + KeyPair pair = CryptUtil.generateRandomKeyPair(512); + byte[] encodedPrivate = pair.getPrivate().getEncoded(); + byte[] encodedPublic = pair.getPublic().getEncoded(); + return new AndroidSharedKeyRecord(PropertyUtils.genUUID(), encodedPrivate, encodedPublic); } private String getKeyId() { From ebb96fda99c9ba10e74d9e23804946d2f757bf22 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 29 Nov 2023 19:12:38 +0200 Subject: [PATCH 11/42] Remove KeyStore reference from CommCare Platform --- app/src/org/commcare/utils/AndroidCommCarePlatform.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/org/commcare/utils/AndroidCommCarePlatform.java b/app/src/org/commcare/utils/AndroidCommCarePlatform.java index 4383fe084c..a1beba2039 100644 --- a/app/src/org/commcare/utils/AndroidCommCarePlatform.java +++ b/app/src/org/commcare/utils/AndroidCommCarePlatform.java @@ -37,10 +37,6 @@ public class AndroidCommCarePlatform extends CommCarePlatform { private final CommCareApp app; private String mUpdateInfoFormXmlns; - { - platformKeyStoreName = "AndroidKeyStore"; - } - public AndroidCommCarePlatform(int majorVersion, int minorVersion, int minimalVersion, CommCareApp app) { super(majorVersion, minorVersion, minimalVersion); xmlnstable = new Hashtable<>(); From db3e33ade467d8534080a3789118832606c58557 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 30 Nov 2023 23:07:50 +0200 Subject: [PATCH 12/42] Refactor --- app/src/org/commcare/CommCareApplication.java | 4 +- .../org/commcare/utils/EncryptionUtils.java | 68 ------------------- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index ff12520cb2..9f9df6f58e 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -141,7 +141,7 @@ import okhttp3.RequestBody; import static org.commcare.util.EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS; -import static org.commcare.utils.EncryptionUtils.generateCryptographicKeyForKeyStore; +import static org.commcare.util.EncryptionUtils.encryptionKeyProvider; public class CommCareApplication extends MultiDexApplication { @@ -253,7 +253,7 @@ public void onCreate() { FirebaseMessagingUtil.verifyToken(); - generateCryptographicKeyForKeyStore(USER_CREDENTIALS_KEY_ALIAS); + encryptionKeyProvider.generateCryptographicKeyInKeyStore(USER_CREDENTIALS_KEY_ALIAS); customiseOkHttp(); } diff --git a/app/src/org/commcare/utils/EncryptionUtils.java b/app/src/org/commcare/utils/EncryptionUtils.java index fed25aeb0a..b975f8db6a 100644 --- a/app/src/org/commcare/utils/EncryptionUtils.java +++ b/app/src/org/commcare/utils/EncryptionUtils.java @@ -1,27 +1,8 @@ package org.commcare.utils; -import android.os.Build; -import android.security.KeyPairGeneratorSpec; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; - -import org.commcare.CommCareApplication; import org.commcare.util.Base64; - -import java.math.BigInteger; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPairGenerator; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.util.GregorianCalendar; - -import javax.crypto.KeyGenerator; -import javax.security.auth.x500.X500Principal; - -import static org.commcare.util.CommCarePlatform.getPlatformKeyStoreName; -import static org.commcare.util.EncryptionUtils.isPlatformKeyStoreAvailable; - /** * Utility class for encrypting submissions during the SaveToDiskTask. @@ -40,53 +21,4 @@ public static String getMD5HashAsString(String plainText) { return ""; } } - - // Generates a cryptrographic key and adds it to the Android KeyStore - public static void generateCryptographicKeyForKeyStore(String keyAlias) { - if (isPlatformKeyStoreAvailable()) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - KeyGenerator keyGenerator = KeyGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_AES, getPlatformKeyStoreName()); - KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build(); - keyGenerator.init(keyGenParameterSpec); - keyGenerator.generateKey(); - } else { - // Because KeyGenParameterSpec was introduced in Android SDK 23, prior versions - // need to resource to KeyPairGenerator which only generates asymmetric keys, - // hence the need to switch to a correspondent algorithm as well, RSA - // TODO: Add link to StackOverflow page - KeyPairGenerator keyGenerator = KeyPairGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_RSA, getPlatformKeyStoreName()); - GregorianCalendar start = new GregorianCalendar(); - GregorianCalendar end = new GregorianCalendar(); - end.add(GregorianCalendar.YEAR, 1); - - KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) - // Key alias to be used to retrieve it from the KeyStore - .setAlias(keyAlias) - // The subject used for the self-signed certificate of the generated pair - .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) - // The serial number used for the self-signed certificate of the - // generated pair - .setSerialNumber(BigInteger.valueOf(1337)) - // Date range of validity for the generated pair - .setStartDate(start.getTime()) - .setEndDate(end.getTime()) - .build(); - - keyGenerator.initialize(keySpec); - keyGenerator.generateKeyPair(); - } - - } catch (NoSuchAlgorithmException | NoSuchProviderException | - InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); - } - } - } } From 5a45854700a78c30c22c98313860de68b91bb13a Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 30 Nov 2023 23:08:30 +0200 Subject: [PATCH 13/42] Add encryption key provider implementation --- .../commcare/utils/EncryptionKeyProvider.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 app/src/org/commcare/utils/EncryptionKeyProvider.java diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java new file mode 100644 index 0000000000..2361eb6e53 --- /dev/null +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -0,0 +1,151 @@ +package org.commcare.utils; + +import android.os.Build; +import android.security.KeyPairGeneratorSpec; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import org.commcare.CommCareApplication; +import org.commcare.util.EncryptionKeyAndTransformation; +import org.commcare.util.EncryptionUtils; +import org.commcare.util.IEncryptionKeyProvider; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.util.GregorianCalendar; + +import javax.crypto.KeyGenerator; +import javax.security.auth.x500.X500Principal; + +public class EncryptionKeyProvider implements IEncryptionKeyProvider { + + private static final String KEYSTORE_NAME = "AndroidKeyStore"; + + private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; + + private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; + private static KeyStore keystoreSingleton = null; + + private static KeyStore getKeyStore() throws KeyStoreException, CertificateException, + IOException, NoSuchAlgorithmException { + if (keystoreSingleton == null) { + keystoreSingleton = KeyStore.getInstance(KEYSTORE_NAME); + keystoreSingleton.load(null); + } + return keystoreSingleton; + } + + @Override + public EncryptionKeyAndTransformation retrieveKeyFromKeyStore(String keyAlias, + EncryptionUtils.CryptographicOperation operation) + throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException, CertificateException, IOException { + Key key; + if (getKeyStore().containsAlias(keyAlias)) { + KeyStore.Entry keyEntry = getKeyStore().getEntry(keyAlias, null); + if (keyEntry instanceof KeyStore.PrivateKeyEntry) { + if (operation == EncryptionUtils.CryptographicOperation.Encryption) { + key = ((KeyStore.PrivateKeyEntry)keyEntry).getCertificate().getPublicKey(); + } else { + key = ((KeyStore.PrivateKeyEntry)keyEntry).getPrivateKey(); + } + } else { + key = ((KeyStore.SecretKeyEntry)keyEntry).getSecretKey(); + } + } else { + throw new KeyStoreException("Key not found in KeyStore"); + } + if (key != null) + return new EncryptionKeyAndTransformation(key, getTransformationString(key.getAlgorithm())); + else + return null; + } + + // Generates a cryptrographic key and adds it to the Android KeyStore + public void generateCryptographicKeyInKeyStore(String keyAlias) { + if (isKeyStoreAvailable()) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + KeyGenerator keyGenerator = KeyGenerator + .getInstance(getAESKeyAlgorithmRepresentation(), KEYSTORE_NAME); + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .build(); + keyGenerator.init(keyGenParameterSpec); + keyGenerator.generateKey(); + } else { + // Because KeyGenParameterSpec was introduced in Android SDK 23, prior versions + // need to resource to KeyPairGenerator which only generates asymmetric keys, + // hence the need to switch to a correspondent algorithm as well, RSA + // TODO: Add link to StackOverflow page + KeyPairGenerator keyGenerator = KeyPairGenerator + .getInstance(getRSAKeyAlgorithmRepresentation(), KEYSTORE_NAME); + GregorianCalendar start = new GregorianCalendar(); + GregorianCalendar end = new GregorianCalendar(); + end.add(GregorianCalendar.YEAR, 100); + + KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) + // Key alias to be used to retrieve it from the KeyStore + .setAlias(keyAlias) + // The subject used for the self-signed certificate of the generated pair + .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) + // The serial number used for the self-signed certificate of the + // generated pair + .setSerialNumber(BigInteger.valueOf(1337)) + // Date range of validity for the generated pair + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + + keyGenerator.initialize(keySpec); + keyGenerator.generateKeyPair(); + } + + } catch (NoSuchAlgorithmException | NoSuchProviderException | + InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException("KeyStore not available"); + } + } + + @Override + public boolean isKeyStoreAvailable() { + return Security.getProvider(KEYSTORE_NAME) != null; + } + + @Override + public String getAESKeyAlgorithmRepresentation() { + return KeyProperties.KEY_ALGORITHM_AES; + } + + @Override + public String getRSAKeyAlgorithmRepresentation() { + return KeyProperties.KEY_ALGORITHM_RSA; + } + + @Override + public String getTransformationString(String algorithm) { + String transformation = null; + if (algorithm.equals(getRSAKeyAlgorithmRepresentation())) { + transformation = "RSA/ECB/PKCS1Padding"; + } else if (algorithm.equals(getAESKeyAlgorithmRepresentation())) { + transformation = String.format("%s/%s/%s", algorithm, BLOCK_MODE, PADDING); + } + // This will cause an error if null + return transformation; + } + +} From d328a05c59e6de6839214929a58c439d83008d7e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 30 Nov 2023 23:18:37 +0200 Subject: [PATCH 14/42] Add service providers configuration --- .../META-INF/services/org.commcare.util.IEncryptionKeyProvider | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider diff --git a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider new file mode 100644 index 0000000000..59ecab0fae --- /dev/null +++ b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider @@ -0,0 +1 @@ +org.commcare.utils.EncryptionKeyProvider From ecb938b26cd4a01f77653b23d8df841cfa5c1c52 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 5 Dec 2023 20:41:18 +0200 Subject: [PATCH 15/42] Refactor --- app/unit-tests/src/org/commcare/CommCareTestApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/unit-tests/src/org/commcare/CommCareTestApplication.java b/app/unit-tests/src/org/commcare/CommCareTestApplication.java index eb38ef32cb..76cc4d8f48 100644 --- a/app/unit-tests/src/org/commcare/CommCareTestApplication.java +++ b/app/unit-tests/src/org/commcare/CommCareTestApplication.java @@ -211,7 +211,7 @@ public void startUserSession(byte[] symetricKey, UserKeyRecord record, boolean r } if (user != null) { user.setCachedPwd(cachedUserPassword); - user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateSemiRandomKey().getEncoded(), cachedUserPassword)); + user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateRandomSecretKey().getEncoded(), cachedUserPassword)); } ccService.startSession(user, record); CommCareApplication.instance().setTestingService(ccService); From d8f3b6cc91001911872d3575e42383041d933f0e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 5 Dec 2023 20:43:05 +0200 Subject: [PATCH 16/42] Move KeyStore name to global constants --- app/src/org/commcare/utils/EncryptionKeyProvider.java | 4 ++-- app/src/org/commcare/utils/GlobalConstants.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index 2361eb6e53..7a2e8e578e 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -27,9 +27,9 @@ import javax.crypto.KeyGenerator; import javax.security.auth.x500.X500Principal; -public class EncryptionKeyProvider implements IEncryptionKeyProvider { +import static org.commcare.utils.GlobalConstants.KEYSTORE_NAME; - private static final String KEYSTORE_NAME = "AndroidKeyStore"; +public class EncryptionKeyProvider implements IEncryptionKeyProvider { private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; diff --git a/app/src/org/commcare/utils/GlobalConstants.java b/app/src/org/commcare/utils/GlobalConstants.java index 7b8320f568..01438309e6 100644 --- a/app/src/org/commcare/utils/GlobalConstants.java +++ b/app/src/org/commcare/utils/GlobalConstants.java @@ -56,4 +56,6 @@ public class GlobalConstants { public static final String TRUSTED_SOURCE_PUBLIC_KEY = BuildConfig.TRUSTED_SOURCE_PUBLIC_KEY; public static final String SMS_INSTALL_KEY_STRING = "[commcare app - do not delete]"; + + public static final String KEYSTORE_NAME = "AndroidKeyStore"; } From 021f2d9c98cb0bdfe48722af7f02e5c93ee6a38e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 5 Dec 2023 22:15:40 +0200 Subject: [PATCH 17/42] Refactor --- app/src/org/commcare/CommCareApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 9f9df6f58e..5c89884513 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -141,7 +141,7 @@ import okhttp3.RequestBody; import static org.commcare.util.EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS; -import static org.commcare.util.EncryptionUtils.encryptionKeyProvider; +import static org.commcare.util.EncryptionUtils.getEncryptionKeyProvider; public class CommCareApplication extends MultiDexApplication { @@ -253,7 +253,7 @@ public void onCreate() { FirebaseMessagingUtil.verifyToken(); - encryptionKeyProvider.generateCryptographicKeyInKeyStore(USER_CREDENTIALS_KEY_ALIAS); + getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(USER_CREDENTIALS_KEY_ALIAS); customiseOkHttp(); } From 7bfd5af673304bf2bc3986b90cd0b3187b26170c Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 6 Dec 2023 12:53:01 +0200 Subject: [PATCH 18/42] Add mock Key generator and key store --- .../utils/MockAndroidKeyStoreProvider.java | 26 ++++ .../org/commcare/utils/MockKeyGenerator.java | 13 ++ .../commcare/utils/MockKeyGeneratorSpi.java | 65 +++++++++ .../src/org/commcare/utils/MockKeyStore.java | 126 ++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java create mode 100644 app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java create mode 100644 app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java create mode 100644 app/unit-tests/src/org/commcare/utils/MockKeyStore.java diff --git a/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java b/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java new file mode 100644 index 0000000000..6a2adb97f2 --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java @@ -0,0 +1,26 @@ +package org.commcare.utils; + +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; + +public class MockAndroidKeyStoreProvider extends Provider { + + { + put("KeyStore.AndroidKeyStore", MockKeyStore.class.getName()); + } + + protected MockAndroidKeyStoreProvider() { + super(GlobalConstants.KEYSTORE_NAME, 1.0, "Mock AndroidKeyStore provider"); + } + + public static void registerProvider() throws NoSuchAlgorithmException { + Security.addProvider(new MockAndroidKeyStoreProvider()); + } + + public static void deregisterProvider() { + if (Security.getProvider(GlobalConstants.KEYSTORE_NAME) != null) { + Security.removeProvider(GlobalConstants.KEYSTORE_NAME); + } + } +} diff --git a/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java b/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java new file mode 100644 index 0000000000..47ee908a74 --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java @@ -0,0 +1,13 @@ +package org.commcare.utils; + +import java.security.NoSuchAlgorithmException; +import java.security.Security; + +import javax.crypto.KeyGenerator; + +public class MockKeyGenerator extends KeyGenerator { + + public MockKeyGenerator() throws NoSuchAlgorithmException { + super(new MockKeyGeneratorSpi() , Security.getProvider(GlobalConstants.KEYSTORE_NAME), "AES"); + } +} diff --git a/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java b/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java new file mode 100644 index 0000000000..5d16b991e3 --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java @@ -0,0 +1,65 @@ +package org.commcare.utils; + +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.KeyGenerator; +import javax.crypto.KeyGeneratorSpi; +import javax.crypto.SecretKey; + +public class MockKeyGeneratorSpi extends KeyGeneratorSpi { + private KeyGenerator wrappedKeyGenerator; + private KeyStore keyStore; + private KeyGenParameterSpec spec = null; + + { + try { + wrappedKeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES); + keyStore = KeyStore.getInstance(GlobalConstants.KEYSTORE_NAME); + keyStore.load(null); + } catch (CertificateException | IOException | NoSuchAlgorithmException | + KeyStoreException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) + throws InvalidAlgorithmParameterException { + if (params == null || !(params instanceof KeyGenParameterSpec)) { + throw new InvalidAlgorithmParameterException( + String.format("Cannot initialize without a %s parameter", KeyGenParameterSpec.class.getName())); + } + spec = (KeyGenParameterSpec)params; + } + + @Override + protected void engineInit(int keysize, SecureRandom random) { + // Do nothing, this is a mock key generator + } + + @Override + protected void engineInit(SecureRandom random) { + // Do nothing, this is a mock key generator + } + + @Override + protected SecretKey engineGenerateKey() { + SecretKey secretKey = wrappedKeyGenerator.generateKey(); + try { + keyStore.setKeyEntry(spec.getKeystoreAlias(), secretKey, null, null); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + return secretKey; + } +} diff --git a/app/unit-tests/src/org/commcare/utils/MockKeyStore.java b/app/unit-tests/src/org/commcare/utils/MockKeyStore.java new file mode 100644 index 0000000000..8aa247417f --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/MockKeyStore.java @@ -0,0 +1,126 @@ +package org.commcare.utils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.SecretKey; + +public class MockKeyStore extends KeyStoreSpi { + + private static HashMap keys = new HashMap<>(); + private static HashMap certs = new HashMap<>(); + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) { + keys.put(alias, key); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + keys.remove(alias); + certs.remove(alias); + } + + @Override + public boolean engineContainsAlias(String alias) { + return keys.containsKey(alias) || certs.containsKey(alias); + } + + @Override + public int engineSize() { + Set allKeys = new HashSet<>(); + allKeys.addAll(keys.keySet()); + allKeys.addAll(certs.keySet()); + return allKeys.size(); + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return keys.containsKey(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return certs.containsKey(alias); + } + + @Override + public KeyStore.Entry engineGetEntry(String alias, KeyStore.ProtectionParameter protParam) { + Key key = keys.get(alias); + if (key != null) { + if (key instanceof SecretKey){ + return new KeyStore.SecretKeyEntry((SecretKey)key); + } else if (key instanceof PrivateKey) { + return new KeyStore.PrivateKeyEntry((PrivateKey)key, null); + } + } + Certificate cert = certs.get(alias); + if (cert != null) { + return new KeyStore.TrustedCertificateEntry(cert); + } + throw new UnsupportedOperationException(String.format("No alias found in keys or certs, alias=%s", alias)); + } + + @Override + public Key engineGetKey(String alias, char[] password) { + return null; + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + return null; + } + + @Override + public Certificate engineGetCertificate(String alias) { + return null; + } + + @Override + public Date engineGetCreationDate(String alias) { + return null; + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + return null; + } + + @Override + public void engineStore(OutputStream stream, char[] password) { + // Do nothing, this is a mock key store + } + + @Override + public void engineLoad(InputStream stream, char[] password) { + // Do nothing, this is a mock key store + } + + @Override + public Enumeration engineAliases() { + return null; + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) { + // Do nothing, this is a mock key store for secret keys + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) { + // Do nothing, this is a mock key store for secret keys + } +} + + From 8c741930c9291db96d2453f3cdcb5335d0868a47 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 6 Dec 2023 12:53:23 +0200 Subject: [PATCH 19/42] Add unit test --- .../utils/EncryptedCredentialsInMemoryTest.kt | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt diff --git a/app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt new file mode 100644 index 0000000000..caac07271f --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt @@ -0,0 +1,97 @@ +package org.commcare.utils; + +import android.security.keystore.KeyGenParameterSpec +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.every +import io.mockk.mockk +import org.commcare.CommCareApplication +import org.commcare.CommCareTestApplication +import org.commcare.android.util.TestAppInstaller +import org.javarosa.core.model.User +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.commcare.util.EncryptionUtils; + + +@Config(application = CommCareTestApplication::class) +@RunWith(AndroidJUnit4::class) +public class EncryptCredentialsInMemoryTest { + + @Before + fun setup() { + TestAppInstaller.installAppAndUser( + "jr://resource/commcare-apps/update_tests/base_app/profile.ccpr", + TEST_USER, TEST_PASS) + + // Set production encryption key provider + EncryptionUtils.setEncryptionKeyProvider(EncryptionKeyProvider()) + } + + @Test + fun saveUsernameWithKeyStoreAndReadWithout_shouldPass() { + // confirm that there is no android key store available + Assert.assertFalse(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) + + // register mock Android key store provider, this is when the key store becomes available + MockAndroidKeyStoreProvider.registerProvider(); + + // generate key to encrypt User credentials in the key store + generateUserCredentialKey() + + // assert that the android key store is available + Assert.assertTrue(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) + + // login with the Android key store available + TestAppInstaller.login(TEST_USER, TEST_PASS) + + // retrieve the logged in user, this should be using the encrypted version + var user = CommCareApplication.instance().getSession().getLoggedInUser() + + // save the same username and store the username for future comparison + user.setUsername(TEST_USER) + CommCareApplication.instance().getRawStorage("USER", User::class.java, CommCareApplication.instance().getUserDbHandle()).write(user) + var username = user.username + + // close the user session + CommCareApplication.instance().closeUserSession() + + // deregister the mock Android key store provider, key store is no longer available + MockAndroidKeyStoreProvider.deregisterProvider() + + // confirm that the key store is no longer available + Assert.assertFalse(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) + + // login once again, this time without the keystore + TestAppInstaller.login(TEST_USER, TEST_PASS) + + // retrieve the current logged in user + user = CommCareApplication.instance().getSession().getLoggedInUser() + + // confirm that the previously captured username matches the current user's + Assert.assertEquals(username, user.username) + } + + @After + fun restore(){ + EncryptionUtils.reloadEncryptionKeyProvider() + } + + private fun generateUserCredentialKey(){ + var mockKeyGenParameterSpec = mockk(); + every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS + + // generate key using mock key generator + var mockKeyGenerator = MockKeyGenerator() + mockKeyGenerator.init(mockKeyGenParameterSpec) + mockKeyGenerator.generateKey() + } + + companion object { + private const val TEST_USER = "test" + private const val TEST_PASS = "123" + } +} From 4e137b6630425db44bddc612aac63e4de649455f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 6 Dec 2023 13:35:32 +0200 Subject: [PATCH 20/42] Lint --- app/src/org/commcare/CommCareApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 5c89884513..fa25d84bc8 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -231,6 +231,8 @@ public void onCreate() { setRoots(); prepareTemporaryStorage(); + getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(USER_CREDENTIALS_KEY_ALIAS); + if (LegacyInstallUtils.checkForLegacyInstall(this)) { dbState = STATE_LEGACY_DETECTED; } else { @@ -253,8 +255,6 @@ public void onCreate() { FirebaseMessagingUtil.verifyToken(); - getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(USER_CREDENTIALS_KEY_ALIAS); - customiseOkHttp(); } From 275be76d33340311b856d1abd535fca6517a923f Mon Sep 17 00:00:00 2001 From: Dave Viggiano Date: Wed, 1 Nov 2023 13:36:36 -0400 Subject: [PATCH 21/42] Added cccStaging build flavor to support Connect production and staging versions. --- app/build.gradle | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index e191108cfc..985224d920 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -272,6 +272,9 @@ android { buildConfigField "String", "HQ_API_USERNAME", "\"${project.ext.HQ_API_USERNAME}\"" buildConfigField "String", "HQ_API_PASSWORD", "\"${project.ext.HQ_API_PASSWORD}\"" + buildConfigField "String", "FIREBASE_DATABASE_URL", "\"${project.ext.FIREBASE_DATABASE_URL}\"" + + buildConfigField "String", "CCC_HOST", "\"connect.dimagi.com\"" testInstrumentationRunner "org.commcare.CommCareJUnitRunner" } @@ -331,6 +334,10 @@ android { manifest.srcFile 'CommcareAndroidManifest.xml' } + cccStaging { + manifest.srcFile 'CommcareAndroidManifest.xml' + } + standalone { res.srcDirs = ['standalone/res'] assets.srcDirs = ['standalone/assets'] @@ -381,6 +388,15 @@ android { resValue "string", "application_name", applicationName } + + cccStaging { + buildConfigField "String", "CCC_HOST", "\"connect-staging.dimagi.com\"" + + // set the app name + def applicationName = "CommCare (CCC Staging)" + resValue "string", "application_name", applicationName + } + standalone { // Builds commcare w/ ccz app packaged in the apk. // Must be invoked from command-line w/ args pointing to app domain & From 28493bf118fa293037848b0308d78751d2d99a85 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 7 Dec 2023 09:20:27 +0200 Subject: [PATCH 22/42] Add gradle task to register service providers --- .gitignore | 3 +- app/build.gradle | 32 +++++++++++++++---- .../org.commcare.util.IEncryptionKeyProvider | 1 - 3 files changed, 27 insertions(+), 9 deletions(-) delete mode 100644 app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider diff --git a/.gitignore b/.gitignore index 21c6b82b52..63c6ee042e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ app/libs/javarosa-libraries.jar build/ **/*~ app/google-services.json -app/fabric.properties \ No newline at end of file +app/fabric.properties +app/src/META-INF/ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5e119e9b23..1208a97204 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,17 +161,21 @@ ext { HQ_API_PASSWORD = project.properties['HQ_API_PASSWORD'] ?: "" TEST_BUILD_TYPE = project.properties['TEST_BUILD_TYPE'] ?: "debug" FIREBASE_DATABASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: "" + + // properties related to Service providers + SERVICE_PROVIDERS = ["org.commcare.util.IEncryptionKeyProvider" : "org.commcare.utils.EncryptionKeyProvider"] + SERVICE_PROVIDERS_REL_DIR = "META-INF/services" } afterEvaluate { // Hack to get assets to show up in robolectric tests; try to eventually remove this - preCommcareDebugUnitTestBuild.dependsOn mergeCommcareDebugAssets - processStandaloneDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile - processStandaloneReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile - processLtsDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile - processLtsReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile - processCommcareDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile - processCommcareReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile + preCommcareDebugUnitTestBuild.dependsOn mergeCommcareDebugAssets, registerServiceProviders + processStandaloneDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders + processStandaloneReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders + processLtsDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders + processLtsReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders + processCommcareDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders + processCommcareReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders } /** @@ -632,3 +636,17 @@ downloadLicenses { includeProjectDependencies = true dependencyConfiguration = 'compile' } + +task registerServiceProviders { + doLast { + def servProvAbsPath = android.sourceSets.main.java.srcDirs[0].path + File.separator + project.ext.SERVICE_PROVIDERS_REL_DIR + project.ext.SERVICE_PROVIDERS.each { servProv -> + println(servProvAbsPath + File.separator + "$servProv.key") + def file = new File(servProvAbsPath + File.separator + "$servProv.key") + if(!file.getParentFile().exists()) { + file.getParentFile().mkdirs() + } + file.write("$servProv.value") + } + } +} \ No newline at end of file diff --git a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider deleted file mode 100644 index 59ecab0fae..0000000000 --- a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider +++ /dev/null @@ -1 +0,0 @@ -org.commcare.utils.EncryptionKeyProvider From 6c5caf000c8738838b964c5b9abac18ebe8843d9 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 7 Dec 2023 10:40:20 +0200 Subject: [PATCH 23/42] Refactor --- ...t.kt => EncryptCredentialsInMemoryTest.kt} | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) rename app/unit-tests/src/org/commcare/utils/{EncryptedCredentialsInMemoryTest.kt => EncryptCredentialsInMemoryTest.kt} (85%) diff --git a/app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt similarity index 85% rename from app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt rename to app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index caac07271f..a4c291c349 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptedCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -7,6 +7,7 @@ import io.mockk.mockk import org.commcare.CommCareApplication import org.commcare.CommCareTestApplication import org.commcare.android.util.TestAppInstaller +import org.commcare.util.EncryptionUtils import org.javarosa.core.model.User import org.junit.After import org.junit.Assert @@ -14,8 +15,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config -import org.commcare.util.EncryptionUtils; - @Config(application = CommCareTestApplication::class) @RunWith(AndroidJUnit4::class) @@ -24,8 +23,10 @@ public class EncryptCredentialsInMemoryTest { @Before fun setup() { TestAppInstaller.installAppAndUser( - "jr://resource/commcare-apps/update_tests/base_app/profile.ccpr", - TEST_USER, TEST_PASS) + "jr://resource/commcare-apps/update_tests/base_app/profile.ccpr", + TEST_USER, + TEST_PASS + ) // Set production encryption key provider EncryptionUtils.setEncryptionKeyProvider(EncryptionKeyProvider()) @@ -37,7 +38,7 @@ public class EncryptCredentialsInMemoryTest { Assert.assertFalse(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) // register mock Android key store provider, this is when the key store becomes available - MockAndroidKeyStoreProvider.registerProvider(); + MockAndroidKeyStoreProvider.registerProvider() // generate key to encrypt User credentials in the key store generateUserCredentialKey() @@ -53,7 +54,11 @@ public class EncryptCredentialsInMemoryTest { // save the same username and store the username for future comparison user.setUsername(TEST_USER) - CommCareApplication.instance().getRawStorage("USER", User::class.java, CommCareApplication.instance().getUserDbHandle()).write(user) + CommCareApplication.instance().getRawStorage( + "USER", + User::class.java, + CommCareApplication.instance().userDbHandle + ).write(user) var username = user.username // close the user session @@ -69,19 +74,19 @@ public class EncryptCredentialsInMemoryTest { TestAppInstaller.login(TEST_USER, TEST_PASS) // retrieve the current logged in user - user = CommCareApplication.instance().getSession().getLoggedInUser() + user = CommCareApplication.instance().session.loggedInUser // confirm that the previously captured username matches the current user's Assert.assertEquals(username, user.username) } @After - fun restore(){ + fun restore() { EncryptionUtils.reloadEncryptionKeyProvider() } - private fun generateUserCredentialKey(){ - var mockKeyGenParameterSpec = mockk(); + private fun generateUserCredentialKey() { + var mockKeyGenParameterSpec = mockk() every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS // generate key using mock key generator From efe19088809f88c0ae04c32ed1874b088435ecfc Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 7 Dec 2023 10:56:49 +0200 Subject: [PATCH 24/42] Lint --- app/build.gradle | 39 +++++++++---------- .../commcare/utils/EncryptionKeyProvider.java | 24 +++++++++--- .../utils/EncryptCredentialsInMemoryTest.kt | 8 ++-- .../utils/MockAndroidKeyStoreProvider.java | 3 +- .../org/commcare/utils/MockKeyGenerator.java | 3 +- .../commcare/utils/MockKeyGeneratorSpi.java | 6 +-- .../src/org/commcare/utils/MockKeyStore.java | 7 ++-- 7 files changed, 49 insertions(+), 41 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1208a97204..a16098cc00 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,35 +136,34 @@ dependencies { } ext { - // Obtained from ~/.gradle/gradle.properties on build server (mobile agent), or your local // Obtained from ~/.gradle/gradle.properties on build server (mobile agent), or your local // ~/.gradle/gradle.properties file, or loads default empty strings if neither is present - MAPBOX_SDK_API_KEY = project.properties['MAPBOX_SDK_API_KEY'] ?: "" - ANALYTICS_TRACKING_ID_DEV = project.properties['ANALYTICS_TRACKING_ID_DEV'] ?: "" - ANALYTICS_TRACKING_ID_LIVE = project.properties['ANALYTICS_TRACKING_ID_LIVE'] ?: "" - GOOGLE_PLAY_MAPS_API_KEY = project.properties['GOOGLE_PLAY_MAPS_API_KEY'] ?: "" - RELEASE_STORE_FILE = project.properties['RELEASE_STORE_FILE'] ?: "." - RELEASE_STORE_PASSWORD = project.properties['RELEASE_STORE_PASSWORD'] ?: "" - RELEASE_KEY_ALIAS = project.properties['RELEASE_KEY_ALIAS'] ?: "" - RELEASE_KEY_PASSWORD = project.properties['RELEASE_KEY_PASSWORD'] ?: "" + MAPBOX_SDK_API_KEY = project.properties['MAPBOX_SDK_API_KEY'] ?: '' + ANALYTICS_TRACKING_ID_DEV = project.properties['ANALYTICS_TRACKING_ID_DEV'] ?: '' + ANALYTICS_TRACKING_ID_LIVE = project.properties['ANALYTICS_TRACKING_ID_LIVE'] ?: '' + GOOGLE_PLAY_MAPS_API_KEY = project.properties['GOOGLE_PLAY_MAPS_API_KEY'] ?: '' + RELEASE_STORE_FILE = project.properties['RELEASE_STORE_FILE'] ?: '.' + RELEASE_STORE_PASSWORD = project.properties['RELEASE_STORE_PASSWORD'] ?: '' + RELEASE_KEY_ALIAS = project.properties['RELEASE_KEY_ALIAS'] ?: '' + RELEASE_KEY_PASSWORD = project.properties['RELEASE_KEY_PASSWORD'] ?: '' TRUSTED_SOURCE_PUBLIC_KEY = project.properties['TRUSTED_SOURCE_PUBLIC_KEY'] ?: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHiuy2ULV4pobkuQN2TEjmR1tn" + "HJ+F335hm/lVdaFQzvBmeq64MUMbumheVLDJaSUiAVzqSHDKJWH01ZQRowqBYjwo" + "ycVSQSeO2glc6XZZ+CJudAPXe8iFWLQp3kBBnBmVcBXCOQFO7aLgQMv4nqKZsLW0" + "HaAJkjpnc165Os+aYwIDAQAB" - GOOGLE_SERVICES_API_KEY = project.properties['GOOGLE_SERVICES_API_KEY'] ?: "" - QA_BETA_APP_ID = "" - STANDALONE_APP_ID = "" - LTS_APP_ID = "" - COMMCARE_APP_ID = "" - HQ_API_USERNAME = project.properties['HQ_API_USERNAME'] ?: "" - HQ_API_PASSWORD = project.properties['HQ_API_PASSWORD'] ?: "" - TEST_BUILD_TYPE = project.properties['TEST_BUILD_TYPE'] ?: "debug" - FIREBASE_DATABASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: "" + GOOGLE_SERVICES_API_KEY = project.properties['GOOGLE_SERVICES_API_KEY'] ?: '' + QA_BETA_APP_ID = '' + STANDALONE_APP_ID = '' + LTS_APP_ID = '' + COMMCARE_APP_ID = '' + HQ_API_USERNAME = project.properties['HQ_API_USERNAME'] ?: '' + HQ_API_PASSWORD = project.properties['HQ_API_PASSWORD'] ?: '' + TEST_BUILD_TYPE = project.properties['TEST_BUILD_TYPE'] ?: 'debug' + FIREBASE_DATABASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: '' // properties related to Service providers - SERVICE_PROVIDERS = ["org.commcare.util.IEncryptionKeyProvider" : "org.commcare.utils.EncryptionKeyProvider"] - SERVICE_PROVIDERS_REL_DIR = "META-INF/services" + SERVICE_PROVIDERS = ['org.commcare.util.IEncryptionKeyProvider' : 'org.commcare.utils.EncryptionKeyProvider'] + SERVICE_PROVIDERS_REL_DIR = 'META-INF/services' } afterEvaluate { diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index 7a2e8e578e..6309ce7119 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -27,12 +27,22 @@ import javax.crypto.KeyGenerator; import javax.security.auth.x500.X500Principal; +import androidx.annotation.RequiresApi; + import static org.commcare.utils.GlobalConstants.KEYSTORE_NAME; +/** + * Class for providing encryption keys backed by Android Keystore + * + * @author dviggiano + */ public class EncryptionKeyProvider implements IEncryptionKeyProvider { + @RequiresApi(api = Build.VERSION_CODES.M) + private static final String ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; + @RequiresApi(api = Build.VERSION_CODES.M) private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; - + @RequiresApi(api = Build.VERSION_CODES.M) private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; private static KeyStore keystoreSingleton = null; @@ -48,7 +58,8 @@ private static KeyStore getKeyStore() throws KeyStoreException, CertificateExcep @Override public EncryptionKeyAndTransformation retrieveKeyFromKeyStore(String keyAlias, EncryptionUtils.CryptographicOperation operation) - throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException, CertificateException, IOException { + throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException, + CertificateException, IOException { Key key; if (getKeyStore().containsAlias(keyAlias)) { KeyStore.Entry keyEntry = getKeyStore().getEntry(keyAlias, null); @@ -64,10 +75,11 @@ public EncryptionKeyAndTransformation retrieveKeyFromKeyStore(String keyAlias, } else { throw new KeyStoreException("Key not found in KeyStore"); } - if (key != null) + if (key != null) { return new EncryptionKeyAndTransformation(key, getTransformationString(key.getAlgorithm())); - else + } else { return null; + } } // Generates a cryptrographic key and adds it to the Android KeyStore @@ -128,12 +140,12 @@ public boolean isKeyStoreAvailable() { @Override public String getAESKeyAlgorithmRepresentation() { - return KeyProperties.KEY_ALGORITHM_AES; + return ALGORITHM; } @Override public String getRSAKeyAlgorithmRepresentation() { - return KeyProperties.KEY_ALGORITHM_RSA; + return "RSA"; } @Override diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index a4c291c349..46f67bebd0 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -1,4 +1,4 @@ -package org.commcare.utils; +package org.commcare.utils import android.security.keystore.KeyGenParameterSpec import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -59,7 +59,7 @@ public class EncryptCredentialsInMemoryTest { User::class.java, CommCareApplication.instance().userDbHandle ).write(user) - var username = user.username + val username = user.username // close the user session CommCareApplication.instance().closeUserSession() @@ -86,11 +86,11 @@ public class EncryptCredentialsInMemoryTest { } private fun generateUserCredentialKey() { - var mockKeyGenParameterSpec = mockk() + val mockKeyGenParameterSpec = mockk() every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS // generate key using mock key generator - var mockKeyGenerator = MockKeyGenerator() + val mockKeyGenerator = MockKeyGenerator() mockKeyGenerator.init(mockKeyGenParameterSpec) mockKeyGenerator.generateKey() } diff --git a/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java b/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java index 6a2adb97f2..8478e3aa4e 100644 --- a/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java +++ b/app/unit-tests/src/org/commcare/utils/MockAndroidKeyStoreProvider.java @@ -1,6 +1,5 @@ package org.commcare.utils; -import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.Security; @@ -14,7 +13,7 @@ protected MockAndroidKeyStoreProvider() { super(GlobalConstants.KEYSTORE_NAME, 1.0, "Mock AndroidKeyStore provider"); } - public static void registerProvider() throws NoSuchAlgorithmException { + public static void registerProvider() { Security.addProvider(new MockAndroidKeyStoreProvider()); } diff --git a/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java b/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java index 47ee908a74..4322c8fc76 100644 --- a/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java +++ b/app/unit-tests/src/org/commcare/utils/MockKeyGenerator.java @@ -1,13 +1,12 @@ package org.commcare.utils; -import java.security.NoSuchAlgorithmException; import java.security.Security; import javax.crypto.KeyGenerator; public class MockKeyGenerator extends KeyGenerator { - public MockKeyGenerator() throws NoSuchAlgorithmException { + public MockKeyGenerator() { super(new MockKeyGeneratorSpi() , Security.getProvider(GlobalConstants.KEYSTORE_NAME), "AES"); } } diff --git a/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java b/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java index 5d16b991e3..224bf53cce 100644 --- a/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java +++ b/app/unit-tests/src/org/commcare/utils/MockKeyGeneratorSpi.java @@ -17,8 +17,8 @@ import javax.crypto.SecretKey; public class MockKeyGeneratorSpi extends KeyGeneratorSpi { - private KeyGenerator wrappedKeyGenerator; - private KeyStore keyStore; + private final KeyGenerator wrappedKeyGenerator; + private final KeyStore keyStore; private KeyGenParameterSpec spec = null; { @@ -35,7 +35,7 @@ public class MockKeyGeneratorSpi extends KeyGeneratorSpi { @Override protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException { - if (params == null || !(params instanceof KeyGenParameterSpec)) { + if (!(params instanceof KeyGenParameterSpec)) { throw new InvalidAlgorithmParameterException( String.format("Cannot initialize without a %s parameter", KeyGenParameterSpec.class.getName())); } diff --git a/app/unit-tests/src/org/commcare/utils/MockKeyStore.java b/app/unit-tests/src/org/commcare/utils/MockKeyStore.java index 8aa247417f..cc54157796 100644 --- a/app/unit-tests/src/org/commcare/utils/MockKeyStore.java +++ b/app/unit-tests/src/org/commcare/utils/MockKeyStore.java @@ -4,7 +4,6 @@ import java.io.OutputStream; import java.security.Key; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.KeyStoreSpi; import java.security.PrivateKey; import java.security.cert.Certificate; @@ -18,8 +17,8 @@ public class MockKeyStore extends KeyStoreSpi { - private static HashMap keys = new HashMap<>(); - private static HashMap certs = new HashMap<>(); + private static final HashMap keys = new HashMap<>(); + private static final HashMap certs = new HashMap<>(); @Override public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) { @@ -27,7 +26,7 @@ public void engineSetKeyEntry(String alias, Key key, char[] password, Certificat } @Override - public void engineDeleteEntry(String alias) throws KeyStoreException { + public void engineDeleteEntry(String alias) { keys.remove(alias); certs.remove(alias); } From 26f6e1f2f2c88deeb010877649aa265f96186c82 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 7 Dec 2023 18:54:45 +0200 Subject: [PATCH 25/42] Add comments to service provider properties --- app/build.gradle | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a16098cc00..22ea72ab95 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,9 +161,14 @@ ext { TEST_BUILD_TYPE = project.properties['TEST_BUILD_TYPE'] ?: 'debug' FIREBASE_DATABASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: '' - // properties related to Service providers - SERVICE_PROVIDERS = ['org.commcare.util.IEncryptionKeyProvider' : 'org.commcare.utils.EncryptionKeyProvider'] + // properties related to Service providers part of the Java SPI pattern SERVICE_PROVIDERS_REL_DIR = 'META-INF/services' + /** + * Service provider implementations and respective service interfaces should be added to this + * map. The task registerServiceProviders is responsible for iterating over it and create the + * configuration files under META-INF/services + */ + SERVICE_PROVIDERS = ['org.commcare.util.IEncryptionKeyProvider' : 'org.commcare.utils.EncryptionKeyProvider'] } afterEvaluate { From 5c6f8fdbb8772bea1b96e94e7e6e597a1ff0f32a Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 13 Dec 2023 17:41:56 +0200 Subject: [PATCH 26/42] Rename key alias --- app/src/org/commcare/CommCareApplication.java | 4 ++-- .../src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index fa25d84bc8..bfafb62551 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -140,7 +140,7 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; -import static org.commcare.util.EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS; +import static org.commcare.util.EncryptionUtils.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS; import static org.commcare.util.EncryptionUtils.getEncryptionKeyProvider; public class CommCareApplication extends MultiDexApplication { @@ -231,7 +231,7 @@ public void onCreate() { setRoots(); prepareTemporaryStorage(); - getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(USER_CREDENTIALS_KEY_ALIAS); + getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); if (LegacyInstallUtils.checkForLegacyInstall(this)) { dbState = STATE_LEGACY_DETECTED; diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index 46f67bebd0..0a790ea8bc 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -87,7 +87,7 @@ public class EncryptCredentialsInMemoryTest { private fun generateUserCredentialKey() { val mockKeyGenParameterSpec = mockk() - every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionUtils.USER_CREDENTIALS_KEY_ALIAS + every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionUtils.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS // generate key using mock key generator val mockKeyGenerator = MockKeyGenerator() From 8c15e6b85b25c433fbc0af3fc1db517e7fc7f772 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 13 Dec 2023 17:56:32 +0200 Subject: [PATCH 27/42] Revert "Add gradle task to register service providers" This reverts commit 28493bf118fa293037848b0308d78751d2d99a85. --- .gitignore | 3 +- app/build.gradle | 37 ++++--------------- .../org.commcare.util.IEncryptionKeyProvider | 1 + 3 files changed, 9 insertions(+), 32 deletions(-) create mode 100644 app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider diff --git a/.gitignore b/.gitignore index 63c6ee042e..21c6b82b52 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,4 @@ app/libs/javarosa-libraries.jar build/ **/*~ app/google-services.json -app/fabric.properties -app/src/META-INF/ \ No newline at end of file +app/fabric.properties \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 22ea72ab95..7242d25882 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -160,26 +160,17 @@ ext { HQ_API_PASSWORD = project.properties['HQ_API_PASSWORD'] ?: '' TEST_BUILD_TYPE = project.properties['TEST_BUILD_TYPE'] ?: 'debug' FIREBASE_DATABASE_URL = project.properties['FIREBASE_DATABASE_URL'] ?: '' - - // properties related to Service providers part of the Java SPI pattern - SERVICE_PROVIDERS_REL_DIR = 'META-INF/services' - /** - * Service provider implementations and respective service interfaces should be added to this - * map. The task registerServiceProviders is responsible for iterating over it and create the - * configuration files under META-INF/services - */ - SERVICE_PROVIDERS = ['org.commcare.util.IEncryptionKeyProvider' : 'org.commcare.utils.EncryptionKeyProvider'] } afterEvaluate { // Hack to get assets to show up in robolectric tests; try to eventually remove this - preCommcareDebugUnitTestBuild.dependsOn mergeCommcareDebugAssets, registerServiceProviders - processStandaloneDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders - processStandaloneReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders - processLtsDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders - processLtsReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders - processCommcareDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders - processCommcareReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile, registerServiceProviders + preCommcareDebugUnitTestBuild.dependsOn mergeCommcareDebugAssets + processStandaloneDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile + processStandaloneReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile + processLtsDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile + processLtsReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile + processCommcareDebugGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile + processCommcareReleaseGoogleServices.dependsOn injectPropertiesIntoFirebaseConfigFile } /** @@ -640,17 +631,3 @@ downloadLicenses { includeProjectDependencies = true dependencyConfiguration = 'compile' } - -task registerServiceProviders { - doLast { - def servProvAbsPath = android.sourceSets.main.java.srcDirs[0].path + File.separator + project.ext.SERVICE_PROVIDERS_REL_DIR - project.ext.SERVICE_PROVIDERS.each { servProv -> - println(servProvAbsPath + File.separator + "$servProv.key") - def file = new File(servProvAbsPath + File.separator + "$servProv.key") - if(!file.getParentFile().exists()) { - file.getParentFile().mkdirs() - } - file.write("$servProv.value") - } - } -} \ No newline at end of file diff --git a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider new file mode 100644 index 0000000000..59ecab0fae --- /dev/null +++ b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider @@ -0,0 +1 @@ +org.commcare.utils.EncryptionKeyProvider From 6a805fc59f81cd45940c6478e319ac6bfbcbe566 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 14 Dec 2023 10:00:45 +0200 Subject: [PATCH 28/42] Add key algorithms constants --- app/src/org/commcare/utils/EncryptionKeyProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index 6309ce7119..4049bc20d7 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -29,6 +29,7 @@ import androidx.annotation.RequiresApi; +import static org.commcare.util.EncryptionUtils.CC_KEY_ALGORITHM_RSA; import static org.commcare.utils.GlobalConstants.KEYSTORE_NAME; /** @@ -145,7 +146,7 @@ public String getAESKeyAlgorithmRepresentation() { @Override public String getRSAKeyAlgorithmRepresentation() { - return "RSA"; + return CC_KEY_ALGORITHM_RSA; } @Override From 8a339280909c6a10f05e8e59332249566aeafaab Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 14 Dec 2023 10:12:31 +0200 Subject: [PATCH 29/42] Lint --- .../META-INF/services/org.commcare.util.IEncryptionKeyProvider | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider index 59ecab0fae..64255efe97 100644 --- a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider +++ b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider @@ -1 +1 @@ -org.commcare.utils.EncryptionKeyProvider +org.commcare.utils.EncryptionKeyProvider \ No newline at end of file From cd31c2bae41d29f8a3277f8b38014d97a0c0b4a0 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 20 Dec 2023 11:09:20 +0200 Subject: [PATCH 30/42] Refactor EncryptionUtils to EncryptionHelper --- app/src/org/commcare/CommCareApplication.java | 6 +++--- app/src/org/commcare/android/nfc/NfcManager.java | 10 +++++----- .../commcare/android/nfc/NfcReadActivity.java | 6 ++---- .../commcare/android/nfc/NfcWriteActivity.java | 6 ++---- .../commcare/utils/EncryptionKeyProvider.java | 8 ++++---- .../org/commcare/android/nfc/NfcManagerTest.java | 16 ++++++++-------- .../utils/EncryptCredentialsInMemoryTest.kt | 15 ++++++++------- 7 files changed, 32 insertions(+), 35 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index bfafb62551..2a9341d406 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -97,6 +97,7 @@ import org.commcare.tasks.templates.ManagedAsyncTask; import org.commcare.update.UpdateHelper; import org.commcare.update.UpdateWorker; +import org.commcare.util.EncryptionHelper; import org.commcare.util.LogTypes; import org.commcare.utils.AndroidCacheDirSetup; import org.commcare.utils.AndroidCommCarePlatform; @@ -140,8 +141,7 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; -import static org.commcare.util.EncryptionUtils.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS; -import static org.commcare.util.EncryptionUtils.getEncryptionKeyProvider; +import static org.commcare.util.EncryptionHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS; public class CommCareApplication extends MultiDexApplication { @@ -231,7 +231,7 @@ public void onCreate() { setRoots(); prepareTemporaryStorage(); - getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); + (new EncryptionHelper()).getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); if (LegacyInstallUtils.checkForLegacyInstall(this)) { dbState = STATE_LEGACY_DETECTED; diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index a16e694574..0c394ddd0e 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -6,7 +6,7 @@ import android.nfc.NfcAdapter; import org.apache.commons.lang3.StringUtils; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; import javax.annotation.Nullable; @@ -53,12 +53,12 @@ public void disableForegroundDispatch(AppCompatActivity activity) { } } - public String decryptValue(String message) throws EncryptionUtils.EncryptionException { + public String decryptValue(String message) throws EncryptionHelper.EncryptionException { String payloadTag = getPayloadTag(); if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = EncryptionUtils.decryptWithBase64EncodedKey("AES", message, encryptionKey); + message = (new EncryptionHelper()).decryptWithBase64EncodedKey(EncryptionHelper.CC_KEY_ALGORITHM_AES, message, encryptionKey); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -89,13 +89,13 @@ private boolean isEmptyPayloadTag(String payloadTag) { return payloadTag.contentEquals(getEmptyPayloadTag()); } - public String tagAndEncryptPayload(String message) throws EncryptionUtils.EncryptionException { + public String tagAndEncryptPayload(String message) throws EncryptionHelper.EncryptionException { if (StringUtils.isEmpty(message)) { return message; } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = EncryptionUtils.encryptWithBase64EncodedKey("AES", payload, encryptionKey); + payload = (new EncryptionHelper()).encryptWithBase64EncodedKey(EncryptionHelper.CC_KEY_ALGORITHM_AES, payload, encryptionKey); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); diff --git a/app/src/org/commcare/android/nfc/NfcReadActivity.java b/app/src/org/commcare/android/nfc/NfcReadActivity.java index 888266c2ff..fe2de67491 100644 --- a/app/src/org/commcare/android/nfc/NfcReadActivity.java +++ b/app/src/org/commcare/android/nfc/NfcReadActivity.java @@ -1,18 +1,16 @@ package org.commcare.android.nfc; -import android.annotation.TargetApi; import android.content.Intent; import android.nfc.FormatException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.Tag; import android.nfc.tech.Ndef; -import android.os.Build; import android.os.Bundle; import android.util.Pair; import org.commcare.android.javarosa.IntentCallout; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; import java.io.IOException; @@ -95,7 +93,7 @@ private void readFromNfcTag(Tag tag) { finishWithErrorToast("nfc.read.io.error", e); } catch (FormatException e) { finishWithErrorToast("nfc.read.msg.malformed", e); - } catch (EncryptionUtils.EncryptionException e) { + } catch (EncryptionHelper.EncryptionException e) { finishWithErrorToast("nfc.read.msg.decryption.error", e); } catch (NfcManager.InvalidPayloadTagException e) { // payload doesn't have our tag attached, so we should not let the app read this message diff --git a/app/src/org/commcare/android/nfc/NfcWriteActivity.java b/app/src/org/commcare/android/nfc/NfcWriteActivity.java index 44d5dd1659..b2454bb68b 100644 --- a/app/src/org/commcare/android/nfc/NfcWriteActivity.java +++ b/app/src/org/commcare/android/nfc/NfcWriteActivity.java @@ -1,17 +1,15 @@ package org.commcare.android.nfc; -import android.annotation.TargetApi; import android.content.Intent; import android.nfc.FormatException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.Tag; import android.nfc.tech.Ndef; -import android.os.Build; import android.os.Bundle; import org.commcare.android.javarosa.IntentCallout; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; import java.io.IOException; @@ -42,7 +40,7 @@ protected void initFields() { typeForPayload = getIntent().getStringExtra(NFC_PAYLOAD_SINGLE_TYPE_ARG); try { payloadToWrite = nfcManager.tagAndEncryptPayload(getIntent().getStringExtra(NFC_PAYLOAD_TO_WRITE)); - } catch (EncryptionUtils.EncryptionException e) { + } catch (EncryptionHelper.EncryptionException e) { finishWithErrorToast("nfc.write.encryption.error", e); } catch (NfcManager.InvalidPayloadException e) { finishWithErrorToast("nfc.write.payload.error", e); diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index 4049bc20d7..d189c3bbb1 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -7,7 +7,7 @@ import org.commcare.CommCareApplication; import org.commcare.util.EncryptionKeyAndTransformation; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; import org.commcare.util.IEncryptionKeyProvider; import java.io.IOException; @@ -29,7 +29,7 @@ import androidx.annotation.RequiresApi; -import static org.commcare.util.EncryptionUtils.CC_KEY_ALGORITHM_RSA; +import static org.commcare.util.EncryptionHelper.CC_KEY_ALGORITHM_RSA; import static org.commcare.utils.GlobalConstants.KEYSTORE_NAME; /** @@ -58,14 +58,14 @@ private static KeyStore getKeyStore() throws KeyStoreException, CertificateExcep @Override public EncryptionKeyAndTransformation retrieveKeyFromKeyStore(String keyAlias, - EncryptionUtils.CryptographicOperation operation) + EncryptionHelper.CryptographicOperation operation) throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException, CertificateException, IOException { Key key; if (getKeyStore().containsAlias(keyAlias)) { KeyStore.Entry keyEntry = getKeyStore().getEntry(keyAlias, null); if (keyEntry instanceof KeyStore.PrivateKeyEntry) { - if (operation == EncryptionUtils.CryptographicOperation.Encryption) { + if (operation == EncryptionHelper.CryptographicOperation.Encryption) { key = ((KeyStore.PrivateKeyEntry)keyEntry).getCertificate().getPublicKey(); } else { key = ((KeyStore.PrivateKeyEntry)keyEntry).getPrivateKey(); diff --git a/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java b/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java index e64dd0eca4..29bed06661 100644 --- a/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java +++ b/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java @@ -1,7 +1,7 @@ package org.commcare.android.nfc; import org.commcare.CommCareTestApplication; -import org.commcare.util.EncryptionUtils; +import org.commcare.util.EncryptionHelper; import org.junit.Test; import static org.commcare.android.nfc.NfcManager.NFC_ENCRYPTION_SCHEME; @@ -14,7 +14,7 @@ public class NfcManagerTest { private static final String PAYLOAD = "dummy_payload"; @Test - public void payloadEncryptionTest() throws EncryptionUtils.EncryptionException { + public void payloadEncryptionTest() throws EncryptionHelper.EncryptionException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, ENTITY_ID, false); // Empty payload should not have any tag attached @@ -29,7 +29,7 @@ public void payloadEncryptionTest() throws EncryptionUtils.EncryptionException { } @Test - public void emptyEncryptionKeyTest() throws EncryptionUtils.EncryptionException { + public void emptyEncryptionKeyTest() throws EncryptionHelper.EncryptionException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), "", ENTITY_ID, false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); assert encryptedMessage.startsWith(PAYLOAD_DELIMITER + ENTITY_ID + PAYLOAD_DELIMITER); @@ -37,7 +37,7 @@ public void emptyEncryptionKeyTest() throws EncryptionUtils.EncryptionException } @Test - public void emptyEntityIdTest() throws EncryptionUtils.EncryptionException { + public void emptyEntityIdTest() throws EncryptionHelper.EncryptionException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, "", false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); assert encryptedMessage.startsWith(NFC_ENCRYPTION_SCHEME + PAYLOAD_DELIMITER + PAYLOAD_DELIMITER); @@ -45,7 +45,7 @@ public void emptyEntityIdTest() throws EncryptionUtils.EncryptionException { } @Test - public void emptyEncryptionKeyAndEntityIdTest() throws EncryptionUtils.EncryptionException { + public void emptyEncryptionKeyAndEntityIdTest() throws EncryptionHelper.EncryptionException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), "", "", false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); assert encryptedMessage.startsWith(PAYLOAD_DELIMITER + PAYLOAD_DELIMITER); @@ -53,7 +53,7 @@ public void emptyEncryptionKeyAndEntityIdTest() throws EncryptionUtils.Encryptio } @Test(expected = NfcManager.InvalidPayloadTagException.class) - public void readingPayloadWithDifferentTag_shouldFail() throws EncryptionUtils.EncryptionException { + public void readingPayloadWithDifferentTag_shouldFail() throws EncryptionHelper.EncryptionException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, "some_other_id", false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, ENTITY_ID, false) @@ -61,7 +61,7 @@ public void readingPayloadWithDifferentTag_shouldFail() throws EncryptionUtils.E } @Test - public void payloadWithoutTagTest() throws EncryptionUtils.EncryptionException { + public void payloadWithoutTagTest() throws EncryptionHelper.EncryptionException { // Decrypt an old payload without specifying encryptionKey and entityId assert new NfcManager(CommCareTestApplication.instance(), "", "", false) .decryptValue(PAYLOAD).contentEquals(PAYLOAD); @@ -71,4 +71,4 @@ assert new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, ENTITY .decryptValue(PAYLOAD).contentEquals(PAYLOAD); } -} +} \ No newline at end of file diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index 0a790ea8bc..4d253b0932 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -7,7 +7,7 @@ import io.mockk.mockk import org.commcare.CommCareApplication import org.commcare.CommCareTestApplication import org.commcare.android.util.TestAppInstaller -import org.commcare.util.EncryptionUtils +import org.commcare.util.EncryptionHelper import org.javarosa.core.model.User import org.junit.After import org.junit.Assert @@ -20,6 +20,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) public class EncryptCredentialsInMemoryTest { + val encryptionHelper = EncryptionHelper() @Before fun setup() { TestAppInstaller.installAppAndUser( @@ -29,13 +30,13 @@ public class EncryptCredentialsInMemoryTest { ) // Set production encryption key provider - EncryptionUtils.setEncryptionKeyProvider(EncryptionKeyProvider()) + encryptionHelper.setEncryptionKeyProvider(EncryptionKeyProvider()) } @Test fun saveUsernameWithKeyStoreAndReadWithout_shouldPass() { // confirm that there is no android key store available - Assert.assertFalse(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) + Assert.assertFalse(encryptionHelper.getEncryptionKeyProvider().isKeyStoreAvailable) // register mock Android key store provider, this is when the key store becomes available MockAndroidKeyStoreProvider.registerProvider() @@ -44,7 +45,7 @@ public class EncryptCredentialsInMemoryTest { generateUserCredentialKey() // assert that the android key store is available - Assert.assertTrue(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) + Assert.assertTrue(encryptionHelper.getEncryptionKeyProvider().isKeyStoreAvailable) // login with the Android key store available TestAppInstaller.login(TEST_USER, TEST_PASS) @@ -68,7 +69,7 @@ public class EncryptCredentialsInMemoryTest { MockAndroidKeyStoreProvider.deregisterProvider() // confirm that the key store is no longer available - Assert.assertFalse(EncryptionUtils.getEncryptionKeyProvider().isKeyStoreAvailable) + Assert.assertFalse(encryptionHelper.getEncryptionKeyProvider().isKeyStoreAvailable) // login once again, this time without the keystore TestAppInstaller.login(TEST_USER, TEST_PASS) @@ -82,12 +83,12 @@ public class EncryptCredentialsInMemoryTest { @After fun restore() { - EncryptionUtils.reloadEncryptionKeyProvider() + encryptionHelper.reloadEncryptionKeyProvider() } private fun generateUserCredentialKey() { val mockKeyGenParameterSpec = mockk() - every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionUtils.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS + every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS // generate key using mock key generator val mockKeyGenerator = MockKeyGenerator() From 8a97d37c0b01f0026c47c3e89e7296cafae96599 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 19 Jan 2024 13:19:47 +0200 Subject: [PATCH 31/42] Remove RSA option for encryption with encoded string --- app/src/org/commcare/android/nfc/NfcManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index 0c394ddd0e..e4218a9257 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -58,7 +58,7 @@ public String decryptValue(String message) throws EncryptionHelper.EncryptionExc if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = (new EncryptionHelper()).decryptWithBase64EncodedKey(EncryptionHelper.CC_KEY_ALGORITHM_AES, message, encryptionKey); + message = (new EncryptionHelper()).decryptWithBase64EncodedKey(message, encryptionKey); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -95,7 +95,7 @@ public String tagAndEncryptPayload(String message) throws EncryptionHelper.Encry } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = (new EncryptionHelper()).encryptWithBase64EncodedKey(EncryptionHelper.CC_KEY_ALGORITHM_AES, payload, encryptionKey); + payload = (new EncryptionHelper()).encryptWithBase64EncodedKey(payload, encryptionKey); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); From 6edd01d5e6f0ef81984c981d7be38b6d7010bc9c Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 22 Jan 2024 13:45:59 +0200 Subject: [PATCH 32/42] Add EncryptionKeyHelper --- app/src/org/commcare/CommCareApplication.java | 5 +++-- .../commcare/utils/EncryptCredentialsInMemoryTest.kt | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 2a9341d406..fb954782da 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -98,6 +98,7 @@ import org.commcare.update.UpdateHelper; import org.commcare.update.UpdateWorker; import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.AndroidCacheDirSetup; import org.commcare.utils.AndroidCommCarePlatform; @@ -141,7 +142,7 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; -import static org.commcare.util.EncryptionHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS; +import static org.commcare.util.EncryptionKeyHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS; public class CommCareApplication extends MultiDexApplication { @@ -231,7 +232,7 @@ public void onCreate() { setRoots(); prepareTemporaryStorage(); - (new EncryptionHelper()).getEncryptionKeyProvider().generateCryptographicKeyInKeyStore(CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); + (new EncryptionKeyHelper()).generateCryptographicKeyInKeyStore(CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); if (LegacyInstallUtils.checkForLegacyInstall(this)) { dbState = STATE_LEGACY_DETECTED; diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index 4d253b0932..e3ba51f104 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -7,7 +7,7 @@ import io.mockk.mockk import org.commcare.CommCareApplication import org.commcare.CommCareTestApplication import org.commcare.android.util.TestAppInstaller -import org.commcare.util.EncryptionHelper +import org.commcare.util.EncryptionKeyHelper import org.javarosa.core.model.User import org.junit.After import org.junit.Assert @@ -36,7 +36,7 @@ public class EncryptCredentialsInMemoryTest { @Test fun saveUsernameWithKeyStoreAndReadWithout_shouldPass() { // confirm that there is no android key store available - Assert.assertFalse(encryptionHelper.getEncryptionKeyProvider().isKeyStoreAvailable) + Assert.assertFalse(EncryptionKeyHelper.isKeyStoreAvailable) // register mock Android key store provider, this is when the key store becomes available MockAndroidKeyStoreProvider.registerProvider() @@ -45,7 +45,7 @@ public class EncryptCredentialsInMemoryTest { generateUserCredentialKey() // assert that the android key store is available - Assert.assertTrue(encryptionHelper.getEncryptionKeyProvider().isKeyStoreAvailable) + Assert.assertTrue(EncryptionKeyHelper.isKeyStoreAvailable) // login with the Android key store available TestAppInstaller.login(TEST_USER, TEST_PASS) @@ -69,7 +69,7 @@ public class EncryptCredentialsInMemoryTest { MockAndroidKeyStoreProvider.deregisterProvider() // confirm that the key store is no longer available - Assert.assertFalse(encryptionHelper.getEncryptionKeyProvider().isKeyStoreAvailable) + Assert.assertFalse(EncryptionKeyHelper.isKeyStoreAvailable()) // login once again, this time without the keystore TestAppInstaller.login(TEST_USER, TEST_PASS) @@ -88,7 +88,7 @@ public class EncryptCredentialsInMemoryTest { private fun generateUserCredentialKey() { val mockKeyGenParameterSpec = mockk() - every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS + every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionKeyHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS // generate key using mock key generator val mockKeyGenerator = MockKeyGenerator() From 13ffe390225334d3349bd756b3f55753cb279777 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 22 Jan 2024 14:36:58 +0200 Subject: [PATCH 33/42] Refactor IEncryptionKeyProvider to support KeyStore Key generation only --- .../commcare/utils/EncryptionKeyProvider.java | 164 ++++++------------ 1 file changed, 50 insertions(+), 114 deletions(-) diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index d189c3bbb1..22b94e4a73 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -6,22 +6,13 @@ import android.security.keystore.KeyProperties; import org.commcare.CommCareApplication; -import org.commcare.util.EncryptionKeyAndTransformation; -import org.commcare.util.EncryptionHelper; import org.commcare.util.IEncryptionKeyProvider; -import java.io.IOException; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; -import java.security.Key; import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.Security; -import java.security.UnrecoverableEntryException; -import java.security.cert.CertificateException; import java.util.GregorianCalendar; import javax.crypto.KeyGenerator; @@ -29,7 +20,7 @@ import androidx.annotation.RequiresApi; -import static org.commcare.util.EncryptionHelper.CC_KEY_ALGORITHM_RSA; +import static org.commcare.util.EncryptionKeyHelper.CC_KEY_ALGORITHM_RSA; import static org.commcare.utils.GlobalConstants.KEYSTORE_NAME; /** @@ -45,120 +36,65 @@ public class EncryptionKeyProvider implements IEncryptionKeyProvider { private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; @RequiresApi(api = Build.VERSION_CODES.M) private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; - private static KeyStore keystoreSingleton = null; - - private static KeyStore getKeyStore() throws KeyStoreException, CertificateException, - IOException, NoSuchAlgorithmException { - if (keystoreSingleton == null) { - keystoreSingleton = KeyStore.getInstance(KEYSTORE_NAME); - keystoreSingleton.load(null); - } - return keystoreSingleton; - } - - @Override - public EncryptionKeyAndTransformation retrieveKeyFromKeyStore(String keyAlias, - EncryptionHelper.CryptographicOperation operation) - throws KeyStoreException, UnrecoverableEntryException, NoSuchAlgorithmException, - CertificateException, IOException { - Key key; - if (getKeyStore().containsAlias(keyAlias)) { - KeyStore.Entry keyEntry = getKeyStore().getEntry(keyAlias, null); - if (keyEntry instanceof KeyStore.PrivateKeyEntry) { - if (operation == EncryptionHelper.CryptographicOperation.Encryption) { - key = ((KeyStore.PrivateKeyEntry)keyEntry).getCertificate().getPublicKey(); - } else { - key = ((KeyStore.PrivateKeyEntry)keyEntry).getPrivateKey(); - } - } else { - key = ((KeyStore.SecretKeyEntry)keyEntry).getSecretKey(); - } - } else { - throw new KeyStoreException("Key not found in KeyStore"); - } - if (key != null) { - return new EncryptionKeyAndTransformation(key, getTransformationString(key.getAlgorithm())); - } else { - return null; - } - } // Generates a cryptrographic key and adds it to the Android KeyStore public void generateCryptographicKeyInKeyStore(String keyAlias) { - if (isKeyStoreAvailable()) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - KeyGenerator keyGenerator = KeyGenerator - .getInstance(getAESKeyAlgorithmRepresentation(), KEYSTORE_NAME); - KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(BLOCK_MODE) - .setEncryptionPaddings(PADDING) - .build(); - keyGenerator.init(keyGenParameterSpec); - keyGenerator.generateKey(); - } else { - // Because KeyGenParameterSpec was introduced in Android SDK 23, prior versions - // need to resource to KeyPairGenerator which only generates asymmetric keys, - // hence the need to switch to a correspondent algorithm as well, RSA - // TODO: Add link to StackOverflow page - KeyPairGenerator keyGenerator = KeyPairGenerator - .getInstance(getRSAKeyAlgorithmRepresentation(), KEYSTORE_NAME); - GregorianCalendar start = new GregorianCalendar(); - GregorianCalendar end = new GregorianCalendar(); - end.add(GregorianCalendar.YEAR, 100); - - KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) - // Key alias to be used to retrieve it from the KeyStore - .setAlias(keyAlias) - // The subject used for the self-signed certificate of the generated pair - .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) - // The serial number used for the self-signed certificate of the - // generated pair - .setSerialNumber(BigInteger.valueOf(1337)) - // Date range of validity for the generated pair - .setStartDate(start.getTime()) - .setEndDate(end.getTime()) - .build(); - - keyGenerator.initialize(keySpec); - keyGenerator.generateKeyPair(); - } - - } catch (NoSuchAlgorithmException | NoSuchProviderException | - InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + KeyGenerator keyGenerator = KeyGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME); + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .build(); + keyGenerator.init(keyGenParameterSpec); + keyGenerator.generateKey(); + } else { + // Because KeyGenParameterSpec was introduced in Android SDK 23, prior versions + // need to resource to KeyPairGenerator which only generates asymmetric keys, + // hence the need to switch to a correspondent algorithm as well, RSA + // TODO: Add link to StackOverflow page + KeyPairGenerator keyGenerator = KeyPairGenerator + .getInstance(CC_KEY_ALGORITHM_RSA, KEYSTORE_NAME); + GregorianCalendar start = new GregorianCalendar(); + GregorianCalendar end = new GregorianCalendar(); + end.add(GregorianCalendar.YEAR, 100); + + KeyPairGeneratorSpec keySpec = new KeyPairGeneratorSpec.Builder(CommCareApplication.instance()) + // Key alias to be used to retrieve it from the KeyStore + .setAlias(keyAlias) + // The subject used for the self-signed certificate of the generated pair + .setSubject(new X500Principal(String.format("CN=%s", keyAlias))) + // The serial number used for the self-signed certificate of the + // generated pair + .setSerialNumber(BigInteger.valueOf(1337)) + // Date range of validity for the generated pair + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + + keyGenerator.initialize(keySpec); + keyGenerator.generateKeyPair(); } - } else { - throw new RuntimeException("KeyStore not available"); - } - } - - @Override - public boolean isKeyStoreAvailable() { - return Security.getProvider(KEYSTORE_NAME) != null; - } - @Override - public String getAESKeyAlgorithmRepresentation() { - return ALGORITHM; + } catch (NoSuchAlgorithmException | NoSuchProviderException | + InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } } @Override - public String getRSAKeyAlgorithmRepresentation() { - return CC_KEY_ALGORITHM_RSA; + public String getTransformationString() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return String.format("%s/%s/%s", ALGORITHM, BLOCK_MODE, PADDING); + } else { + return "RSA/ECB/PKCS1Padding"; + } } @Override - public String getTransformationString(String algorithm) { - String transformation = null; - if (algorithm.equals(getRSAKeyAlgorithmRepresentation())) { - transformation = "RSA/ECB/PKCS1Padding"; - } else if (algorithm.equals(getAESKeyAlgorithmRepresentation())) { - transformation = String.format("%s/%s/%s", algorithm, BLOCK_MODE, PADDING); - } - // This will cause an error if null - return transformation; + public String getKeyStoreName() { + return "AndroidKeyStore"; } - } From ce30b995de94e0c6c5db956fdc4cbd73c38abe97 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 22 Jan 2024 21:12:18 +0200 Subject: [PATCH 34/42] Add key generation in KeyStore when alias is not found --- app/src/org/commcare/CommCareApplication.java | 4 ---- .../org/commcare/utils/EncryptionKeyProvider.java | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index fb954782da..560edce482 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -97,8 +97,6 @@ import org.commcare.tasks.templates.ManagedAsyncTask; import org.commcare.update.UpdateHelper; import org.commcare.update.UpdateWorker; -import org.commcare.util.EncryptionHelper; -import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.AndroidCacheDirSetup; import org.commcare.utils.AndroidCommCarePlatform; @@ -232,8 +230,6 @@ public void onCreate() { setRoots(); prepareTemporaryStorage(); - (new EncryptionKeyHelper()).generateCryptographicKeyInKeyStore(CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS); - if (LegacyInstallUtils.checkForLegacyInstall(this)) { dbState = STATE_LEGACY_DETECTED; } else { diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index 22b94e4a73..48ad2dd5b4 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -6,10 +6,13 @@ import android.security.keystore.KeyProperties; import org.commcare.CommCareApplication; +import org.commcare.util.EncryptionHelper; import org.commcare.util.IEncryptionKeyProvider; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; @@ -38,7 +41,8 @@ public class EncryptionKeyProvider implements IEncryptionKeyProvider { private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; // Generates a cryptrographic key and adds it to the Android KeyStore - public void generateCryptographicKeyInKeyStore(String keyAlias) { + public Key generateCryptographicKeyInKeyStore(String keyAlias, + EncryptionHelper.CryptographicOperation cryptographicOperation) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyGenerator keyGenerator = KeyGenerator @@ -49,7 +53,7 @@ public void generateCryptographicKeyInKeyStore(String keyAlias) { .setEncryptionPaddings(PADDING) .build(); keyGenerator.init(keyGenParameterSpec); - keyGenerator.generateKey(); + return keyGenerator.generateKey(); } else { // Because KeyGenParameterSpec was introduced in Android SDK 23, prior versions // need to resource to KeyPairGenerator which only generates asymmetric keys, @@ -75,7 +79,12 @@ public void generateCryptographicKeyInKeyStore(String keyAlias) { .build(); keyGenerator.initialize(keySpec); - keyGenerator.generateKeyPair(); + KeyPair keyPair = keyGenerator.generateKeyPair(); + if (cryptographicOperation == EncryptionHelper.CryptographicOperation.Encryption) { + return keyPair.getPublic(); + } else { + return keyPair.getPrivate(); + } } } catch (NoSuchAlgorithmException | NoSuchProviderException | From ff2102d3e32f27762f91a5414eea7e456d39ee38 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 22 Jan 2024 23:19:11 +0200 Subject: [PATCH 35/42] Make EncryptionHelper stateless --- app/src/org/commcare/android/nfc/NfcManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index e4218a9257..f28a77a4fc 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import javax.annotation.Nullable; @@ -58,7 +59,7 @@ public String decryptValue(String message) throws EncryptionHelper.EncryptionExc if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = (new EncryptionHelper()).decryptWithBase64EncodedKey(message, encryptionKey); + message = EncryptionHelper.decryptWithBase64EncodedKey(message, encryptionKey); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -95,7 +96,7 @@ public String tagAndEncryptPayload(String message) throws EncryptionHelper.Encry } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = (new EncryptionHelper()).encryptWithBase64EncodedKey(payload, encryptionKey); + payload = EncryptionHelper.encryptWithBase64EncodedKey(payload, encryptionKey); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); From 1e9326c369a9793da052350e982562a7d0b5260a Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 23 Jan 2024 00:13:28 +0200 Subject: [PATCH 36/42] Add EncryptionKeyException to umbrella exceptions related to encryption key handling --- .../locales/android_translatable_strings.txt | 2 ++ .../org/commcare/android/nfc/NfcManager.java | 6 ++++-- .../commcare/android/nfc/NfcReadActivity.java | 3 +++ .../android/nfc/NfcWriteActivity.java | 3 +++ .../commcare/utils/EncryptionKeyProvider.java | 8 +++++--- .../commcare/android/nfc/NfcManagerTest.java | 19 +++++++++++++------ 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/assets/locales/android_translatable_strings.txt b/app/assets/locales/android_translatable_strings.txt index 3873028d66..3af7a7ff77 100644 --- a/app/assets/locales/android_translatable_strings.txt +++ b/app/assets/locales/android_translatable_strings.txt @@ -924,6 +924,7 @@ nfc.write.type.not.supported=The well-known type you tried to write is not suppo nfc.write.io.error=An IO error occurred while attempting to write the Ndef message to your NFC tag nfc.write.msg.malformed=The Ndef message that you attempted to write was malformed nfc.write.success=NFC write was successful! +nfc.write.encryption.key.error=An error occurred while handling the encryption key nfc.missing.domain=A domain must be provided when specifying a custom type for your NFC action nfc.read.success=NFC read was successful! nfc.read.io.error=An IO error occurred while attempting to read the Ndef message @@ -934,6 +935,7 @@ nfc.read.error.no.ndef=The message on this NFC tag is not of a format that Andro nfc.read.no.data=The provided NFC tag had no data on it to read nfc.read.error.unsupported=The message read from the NFC tag is of a type not supported by CommCare nfc.read.error.mismatch=The message read from the NFC tag does not match the type specified by this app's configuration +nfc.read.msg.decryption.key.error=An error occurred while handling the encryption key reason.for.quarantine.title=Reason for Quarantine reason.for.quarantine.prefix=Quarantine Reason: diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index f28a77a4fc..b360824e48 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -54,7 +54,8 @@ public void disableForegroundDispatch(AppCompatActivity activity) { } } - public String decryptValue(String message) throws EncryptionHelper.EncryptionException { + public String decryptValue(String message) + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { String payloadTag = getPayloadTag(); if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); @@ -90,7 +91,8 @@ private boolean isEmptyPayloadTag(String payloadTag) { return payloadTag.contentEquals(getEmptyPayloadTag()); } - public String tagAndEncryptPayload(String message) throws EncryptionHelper.EncryptionException { + public String tagAndEncryptPayload(String message) + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { if (StringUtils.isEmpty(message)) { return message; } diff --git a/app/src/org/commcare/android/nfc/NfcReadActivity.java b/app/src/org/commcare/android/nfc/NfcReadActivity.java index fe2de67491..aee3437aed 100644 --- a/app/src/org/commcare/android/nfc/NfcReadActivity.java +++ b/app/src/org/commcare/android/nfc/NfcReadActivity.java @@ -11,6 +11,7 @@ import org.commcare.android.javarosa.IntentCallout; import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import java.io.IOException; @@ -98,6 +99,8 @@ private void readFromNfcTag(Tag tag) { } catch (NfcManager.InvalidPayloadTagException e) { // payload doesn't have our tag attached, so we should not let the app read this message finishWithErrorToast("nfc.read.msg.payload.tag.error"); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + finishWithErrorToast("nfc.read.msg.decryption.key.error", e); } finally { try { ndefObject.close(); diff --git a/app/src/org/commcare/android/nfc/NfcWriteActivity.java b/app/src/org/commcare/android/nfc/NfcWriteActivity.java index b2454bb68b..d27712cf7e 100644 --- a/app/src/org/commcare/android/nfc/NfcWriteActivity.java +++ b/app/src/org/commcare/android/nfc/NfcWriteActivity.java @@ -10,6 +10,7 @@ import org.commcare.android.javarosa.IntentCallout; import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import java.io.IOException; @@ -44,6 +45,8 @@ protected void initFields() { finishWithErrorToast("nfc.write.encryption.error", e); } catch (NfcManager.InvalidPayloadException e) { finishWithErrorToast("nfc.write.payload.error", e); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + finishWithErrorToast("nfc.write.encryption.error", e); } } diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/EncryptionKeyProvider.java index 48ad2dd5b4..30308205a3 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/EncryptionKeyProvider.java @@ -7,6 +7,7 @@ import org.commcare.CommCareApplication; import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.IEncryptionKeyProvider; import java.math.BigInteger; @@ -41,8 +42,10 @@ public class EncryptionKeyProvider implements IEncryptionKeyProvider { private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; // Generates a cryptrographic key and adds it to the Android KeyStore + @Override public Key generateCryptographicKeyInKeyStore(String keyAlias, - EncryptionHelper.CryptographicOperation cryptographicOperation) { + EncryptionHelper.CryptographicOperation cryptographicOperation) + throws EncryptionKeyHelper.EncryptionKeyException{ try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyGenerator keyGenerator = KeyGenerator @@ -86,10 +89,9 @@ public Key generateCryptographicKeyInKeyStore(String keyAlias, return keyPair.getPrivate(); } } - } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); + throw new EncryptionKeyHelper.EncryptionKeyException("Key generation failed: ", e); } } diff --git a/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java b/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java index 29bed06661..897f698ccf 100644 --- a/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java +++ b/app/unit-tests/src/org/commcare/android/nfc/NfcManagerTest.java @@ -2,6 +2,7 @@ import org.commcare.CommCareTestApplication; import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; import org.junit.Test; import static org.commcare.android.nfc.NfcManager.NFC_ENCRYPTION_SCHEME; @@ -14,7 +15,8 @@ public class NfcManagerTest { private static final String PAYLOAD = "dummy_payload"; @Test - public void payloadEncryptionTest() throws EncryptionHelper.EncryptionException { + public void payloadEncryptionTest() + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, ENTITY_ID, false); // Empty payload should not have any tag attached @@ -29,7 +31,8 @@ public void payloadEncryptionTest() throws EncryptionHelper.EncryptionException } @Test - public void emptyEncryptionKeyTest() throws EncryptionHelper.EncryptionException { + public void emptyEncryptionKeyTest() + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), "", ENTITY_ID, false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); assert encryptedMessage.startsWith(PAYLOAD_DELIMITER + ENTITY_ID + PAYLOAD_DELIMITER); @@ -37,7 +40,8 @@ public void emptyEncryptionKeyTest() throws EncryptionHelper.EncryptionException } @Test - public void emptyEntityIdTest() throws EncryptionHelper.EncryptionException { + public void emptyEntityIdTest() + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, "", false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); assert encryptedMessage.startsWith(NFC_ENCRYPTION_SCHEME + PAYLOAD_DELIMITER + PAYLOAD_DELIMITER); @@ -45,7 +49,8 @@ public void emptyEntityIdTest() throws EncryptionHelper.EncryptionException { } @Test - public void emptyEncryptionKeyAndEntityIdTest() throws EncryptionHelper.EncryptionException { + public void emptyEncryptionKeyAndEntityIdTest() + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), "", "", false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); assert encryptedMessage.startsWith(PAYLOAD_DELIMITER + PAYLOAD_DELIMITER); @@ -53,7 +58,8 @@ public void emptyEncryptionKeyAndEntityIdTest() throws EncryptionHelper.Encrypti } @Test(expected = NfcManager.InvalidPayloadTagException.class) - public void readingPayloadWithDifferentTag_shouldFail() throws EncryptionHelper.EncryptionException { + public void readingPayloadWithDifferentTag_shouldFail() + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { NfcManager nfcManager = new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, "some_other_id", false); String encryptedMessage = nfcManager.tagAndEncryptPayload(PAYLOAD); new NfcManager(CommCareTestApplication.instance(), ENCRYPTION_KEY, ENTITY_ID, false) @@ -61,7 +67,8 @@ public void readingPayloadWithDifferentTag_shouldFail() throws EncryptionHelper. } @Test - public void payloadWithoutTagTest() throws EncryptionHelper.EncryptionException { + public void payloadWithoutTagTest() + throws EncryptionHelper.EncryptionException, EncryptionKeyHelper.EncryptionKeyException { // Decrypt an old payload without specifying encryptionKey and entityId assert new NfcManager(CommCareTestApplication.instance(), "", "", false) .decryptValue(PAYLOAD).contentEquals(PAYLOAD); From b1af601e920e585334406c31ce304bcd7d136a62 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 23 Jan 2024 14:17:02 +0200 Subject: [PATCH 37/42] Rename IEncrypitonKeyProvider to IKeyStoreEncryptionKeyProvider --- .../services/org.commcare.util.IEncryptionKeyProvider | 1 - .../services/org.commcare.util.IKeyStoreEncryptionKeyProvider | 1 + ...ionKeyProvider.java => KeyStoreEncryptionKeyProvider.java} | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider create mode 100644 app/src/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider rename app/src/org/commcare/utils/{EncryptionKeyProvider.java => KeyStoreEncryptionKeyProvider.java} (97%) diff --git a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider b/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider deleted file mode 100644 index 64255efe97..0000000000 --- a/app/src/META-INF/services/org.commcare.util.IEncryptionKeyProvider +++ /dev/null @@ -1 +0,0 @@ -org.commcare.utils.EncryptionKeyProvider \ No newline at end of file diff --git a/app/src/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider b/app/src/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider new file mode 100644 index 0000000000..23a1b0ed00 --- /dev/null +++ b/app/src/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider @@ -0,0 +1 @@ +org.commcare.utils.KeyStoreEncryptionKeyProvider \ No newline at end of file diff --git a/app/src/org/commcare/utils/EncryptionKeyProvider.java b/app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java similarity index 97% rename from app/src/org/commcare/utils/EncryptionKeyProvider.java rename to app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java index 30308205a3..9e31a19382 100644 --- a/app/src/org/commcare/utils/EncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java @@ -8,7 +8,7 @@ import org.commcare.CommCareApplication; import org.commcare.util.EncryptionHelper; import org.commcare.util.EncryptionKeyHelper; -import org.commcare.util.IEncryptionKeyProvider; +import org.commcare.util.IKeyStoreEncryptionKeyProvider; import java.math.BigInteger; import java.security.InvalidAlgorithmParameterException; @@ -32,7 +32,7 @@ * * @author dviggiano */ -public class EncryptionKeyProvider implements IEncryptionKeyProvider { +public class KeyStoreEncryptionKeyProvider implements IKeyStoreEncryptionKeyProvider { @RequiresApi(api = Build.VERSION_CODES.M) private static final String ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; From 7d66b9f2fc261ab63e3b0829663f584ebb005608 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 23 Jan 2024 14:23:57 +0200 Subject: [PATCH 38/42] Add TestKeyStoreEncryptionProvider --- ...mmcare.util.IKeyStoreEncryptionKeyProvider | 1 + .../utils/EncryptCredentialsInMemoryTest.kt | 16 ++----- .../TestKeyStoreEncryptionKeyProvider.java | 45 +++++++++++++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 app/unit-tests/resources/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider create mode 100644 app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java diff --git a/app/unit-tests/resources/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider b/app/unit-tests/resources/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider new file mode 100644 index 0000000000..c4f65b558c --- /dev/null +++ b/app/unit-tests/resources/META-INF/services/org.commcare.util.IKeyStoreEncryptionKeyProvider @@ -0,0 +1 @@ +org.commcare.utils.TestKeyStoreEncryptionKeyProvider \ No newline at end of file diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index e3ba51f104..ddb0a96334 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -8,8 +8,8 @@ import org.commcare.CommCareApplication import org.commcare.CommCareTestApplication import org.commcare.android.util.TestAppInstaller import org.commcare.util.EncryptionKeyHelper +import org.commcare.util.EncryptionHelper import org.javarosa.core.model.User -import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Test @@ -20,7 +20,6 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) public class EncryptCredentialsInMemoryTest { - val encryptionHelper = EncryptionHelper() @Before fun setup() { TestAppInstaller.installAppAndUser( @@ -28,15 +27,13 @@ public class EncryptCredentialsInMemoryTest { TEST_USER, TEST_PASS ) - - // Set production encryption key provider - encryptionHelper.setEncryptionKeyProvider(EncryptionKeyProvider()) } @Test fun saveUsernameWithKeyStoreAndReadWithout_shouldPass() { // confirm that there is no android key store available - Assert.assertFalse(EncryptionKeyHelper.isKeyStoreAvailable) + + Assert.assertFalse(EncryptionKeyHelper.isKeyStoreAvailable()) // register mock Android key store provider, this is when the key store becomes available MockAndroidKeyStoreProvider.registerProvider() @@ -45,7 +42,7 @@ public class EncryptCredentialsInMemoryTest { generateUserCredentialKey() // assert that the android key store is available - Assert.assertTrue(EncryptionKeyHelper.isKeyStoreAvailable) + Assert.assertTrue(EncryptionKeyHelper.isKeyStoreAvailable()) // login with the Android key store available TestAppInstaller.login(TEST_USER, TEST_PASS) @@ -81,11 +78,6 @@ public class EncryptCredentialsInMemoryTest { Assert.assertEquals(username, user.username) } - @After - fun restore() { - encryptionHelper.reloadEncryptionKeyProvider() - } - private fun generateUserCredentialKey() { val mockKeyGenParameterSpec = mockk() every { mockKeyGenParameterSpec.keystoreAlias } returns EncryptionKeyHelper.CC_IN_MEMORY_ENCRYPTION_KEY_ALIAS diff --git a/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java b/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java new file mode 100644 index 0000000000..bd6bca8a1d --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java @@ -0,0 +1,45 @@ +package org.commcare.utils; + +import android.os.Build; +import android.security.keystore.KeyProperties; + +import org.commcare.util.EncryptionHelper; +import org.commcare.util.EncryptionKeyHelper; +import org.commcare.util.IKeyStoreEncryptionKeyProvider; + +import java.security.Key; + +import androidx.annotation.RequiresApi; + +/** + * Class for providing encryption keys backed by Android Keystore for Unit testing + * + * @author avazirna + */ +public class TestKeyStoreEncryptionKeyProvider implements IKeyStoreEncryptionKeyProvider { + + @RequiresApi(api = Build.VERSION_CODES.M) + private static final String ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; + @RequiresApi(api = Build.VERSION_CODES.M) + private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; + @RequiresApi(api = Build.VERSION_CODES.M) + private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; + + // Generates a cryptrographic key and adds it to the Android KeyStore + @Override + public Key generateCryptographicKeyInKeyStore(String keyAlias, + EncryptionHelper.CryptographicOperation cryptographicOperation) + throws EncryptionKeyHelper.EncryptionKeyException { + throw new EncryptionKeyHelper.EncryptionKeyException("KeyStore encryption key generator provider for testing only"); + } + + @Override + public String getTransformationString() { + return String.format("%s/%s/%s", ALGORITHM, BLOCK_MODE, PADDING); + } + + @Override + public String getKeyStoreName() { + return "AndroidKeyStore"; + } +} From 48aa5a523872216c7f1740898762812f077be770 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 23 Jan 2024 15:40:02 +0200 Subject: [PATCH 39/42] Refactor --- app/src/org/commcare/android/nfc/NfcManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/android/nfc/NfcManager.java b/app/src/org/commcare/android/nfc/NfcManager.java index b360824e48..9d0235c2f3 100644 --- a/app/src/org/commcare/android/nfc/NfcManager.java +++ b/app/src/org/commcare/android/nfc/NfcManager.java @@ -60,7 +60,7 @@ public String decryptValue(String message) if (message.startsWith(payloadTag)) { message = message.replace(payloadTag, ""); if (!StringUtils.isEmpty(encryptionKey)) { - message = EncryptionHelper.decryptWithBase64EncodedKey(message, encryptionKey); + message = EncryptionHelper.decryptWithEncodedKey(message, encryptionKey); } } else if (!allowUntaggedRead && !isEmptyPayloadTag(payloadTag)) { throw new InvalidPayloadTagException(); @@ -98,7 +98,7 @@ public String tagAndEncryptPayload(String message) } String payload = message; if (!StringUtils.isEmpty(encryptionKey)) { - payload = EncryptionHelper.encryptWithBase64EncodedKey(payload, encryptionKey); + payload = EncryptionHelper.encryptWithEncodedKey(payload, encryptionKey); } if (payload.contains(PAYLOAD_DELIMITER)) { throw new InvalidPayloadException(); From af8194aa85f5353e2ca4e81ef3ae736ad27164af Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 24 Jan 2024 00:21:48 +0200 Subject: [PATCH 40/42] Lint --- .../org/commcare/utils/KeyStoreEncryptionKeyProvider.java | 7 +++---- .../org/commcare/utils/EncryptCredentialsInMemoryTest.kt | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java b/app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java index 9e31a19382..58cfb8ccd4 100644 --- a/app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java +++ b/app/src/org/commcare/utils/KeyStoreEncryptionKeyProvider.java @@ -25,7 +25,6 @@ import androidx.annotation.RequiresApi; import static org.commcare.util.EncryptionKeyHelper.CC_KEY_ALGORITHM_RSA; -import static org.commcare.utils.GlobalConstants.KEYSTORE_NAME; /** * Class for providing encryption keys backed by Android Keystore @@ -49,7 +48,7 @@ public Key generateCryptographicKeyInKeyStore(String keyAlias, try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyGenerator keyGenerator = KeyGenerator - .getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME); + .getInstance(KeyProperties.KEY_ALGORITHM_AES, getKeyStoreName()); KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(BLOCK_MODE) @@ -63,7 +62,7 @@ public Key generateCryptographicKeyInKeyStore(String keyAlias, // hence the need to switch to a correspondent algorithm as well, RSA // TODO: Add link to StackOverflow page KeyPairGenerator keyGenerator = KeyPairGenerator - .getInstance(CC_KEY_ALGORITHM_RSA, KEYSTORE_NAME); + .getInstance(CC_KEY_ALGORITHM_RSA, getKeyStoreName()); GregorianCalendar start = new GregorianCalendar(); GregorianCalendar end = new GregorianCalendar(); end.add(GregorianCalendar.YEAR, 100); @@ -106,6 +105,6 @@ public String getTransformationString() { @Override public String getKeyStoreName() { - return "AndroidKeyStore"; + return GlobalConstants.KEYSTORE_NAME; } } diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index ddb0a96334..bb68181acb 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -18,7 +18,7 @@ import org.robolectric.annotation.Config @Config(application = CommCareTestApplication::class) @RunWith(AndroidJUnit4::class) -public class EncryptCredentialsInMemoryTest { +class EncryptCredentialsInMemoryTest { @Before fun setup() { @@ -48,7 +48,7 @@ public class EncryptCredentialsInMemoryTest { TestAppInstaller.login(TEST_USER, TEST_PASS) // retrieve the logged in user, this should be using the encrypted version - var user = CommCareApplication.instance().getSession().getLoggedInUser() + var user = CommCareApplication.instance().session.loggedInUser // save the same username and store the username for future comparison user.setUsername(TEST_USER) From db7ef6628ff636d8889518dc3d42688677602b49 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 24 Jan 2024 00:27:41 +0200 Subject: [PATCH 41/42] Bubble up encryption key exceptions --- .../locales/android_translatable_strings.txt | 4 +++- .../activities/InstallFromListActivity.java | 8 +++++++ .../activities/KeyAccessRequestActivity.java | 13 +++++++++++- .../activities/PostRequestActivity.java | 6 ++++++ .../activities/QueryRequestActivity.java | 6 ++++++ .../global/models/AndroidSharedKeyRecord.java | 4 +++- .../models/database/user/DemoUserBuilder.java | 8 +++++-- .../commcare/network/GetAndParseActor.java | 8 +++++++ .../org/commcare/network/HttpCalloutTask.java | 12 ++++++++--- .../network/RemoteDataPullResponse.java | 4 +++- .../commcare/tasks/AsyncRestoreHelper.java | 3 ++- app/src/org/commcare/tasks/DataPullTask.java | 21 ++++++++++++++----- .../org/commcare/tasks/ModernHttpTask.java | 12 ++++++++--- .../commcare/utils/TemplatePrinterUtils.java | 14 ++++++++++++- .../org/commcare/CommCareTestApplication.java | 7 ++++++- 15 files changed, 110 insertions(+), 20 deletions(-) diff --git a/app/assets/locales/android_translatable_strings.txt b/app/assets/locales/android_translatable_strings.txt index 3af7a7ff77..b5af54a02c 100644 --- a/app/assets/locales/android_translatable_strings.txt +++ b/app/assets/locales/android_translatable_strings.txt @@ -196,11 +196,12 @@ post.generic.error=An error occurred while preparing the HTTP request post.dialog.title=Claiming... post.dialog.body=Claiming chosen data from server post.io.error=Error reading server response: ${0} -post.unknown.response=Received unknown resonse code (${0}) from server +post.unknown.response=Received unknown response code (${0}) from server post.client.error=Client-side error (code ${0}) received from network request. post.server.error=Server-side error (code ${0}) received from network request. post.gone.error=Case is not present on server. post.conflict.error=You have already claimed this case. +post.cache.encryption.key.error=Encryption error while processing server response: ${0} version.id.long=CommCare Android, version "${0}"(${1}). App v${5}. CommCare Version ${2}. Build ${3}, built on: ${4} version.id.short=CCDroid:"${0}"(${1}). v${5} CC${2}b[${3}] on ${4} @@ -263,6 +264,7 @@ app.handled.error.explanation=CommCare will now restart. You may need to correct app.key.request.message=Another android application has requested the ability to communicate securely with CommCare. This application will be able to pass information to CommCare and trigger actions (submission, login, etc). Do you want to grant access? app.key.request.grant=Grant Access app.key.request.deny=Deny Access +app.key.request.encryption.key.error=Error during encryption Key generation key.manage.title=Logging in key.manage.start=Logging in diff --git a/app/src/org/commcare/activities/InstallFromListActivity.java b/app/src/org/commcare/activities/InstallFromListActivity.java index f776b5259f..91de17ae95 100644 --- a/app/src/org/commcare/activities/InstallFromListActivity.java +++ b/app/src/org/commcare/activities/InstallFromListActivity.java @@ -32,6 +32,7 @@ import org.commcare.preferences.GlobalPrivilegesManager; import org.commcare.tasks.ModernHttpTask; import org.commcare.tasks.templates.CommCareTaskConnector; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.ConnectivityStatus; import org.commcare.views.UserfacingErrorHandling; @@ -355,6 +356,13 @@ public void handleIOException(IOException exception) { repeatRequestOrShowResults(true, false); } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + Logger.log(LogTypes.TYPE_ERROR_ENCRYPTION_KEY, + "An ENcryptionKeyException was encountered when pocessing available apps request: " + exception.getMessage()); + repeatRequestOrShowResults(true, false); + } + private void handleRequestError(int responseCode, boolean couldBeUserError) { Logger.log(LogTypes.TYPE_ERROR_SERVER_COMMS, "Request to " + urlCurrentlyRequestingFrom + " in get available apps request " + diff --git a/app/src/org/commcare/activities/KeyAccessRequestActivity.java b/app/src/org/commcare/activities/KeyAccessRequestActivity.java index 4a993dee1e..05dcb15288 100644 --- a/app/src/org/commcare/activities/KeyAccessRequestActivity.java +++ b/app/src/org/commcare/activities/KeyAccessRequestActivity.java @@ -4,12 +4,16 @@ import android.os.Bundle; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import org.commcare.CommCareApplication; import org.commcare.android.database.global.models.AndroidSharedKeyRecord; import org.commcare.dalvik.R; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.views.ManagedUi; import org.commcare.views.UiElement; +import org.javarosa.core.services.Logger; +import org.javarosa.core.services.locale.Localization; import androidx.appcompat.app.AppCompatActivity; @@ -34,7 +38,14 @@ protected void onCreate(Bundle savedInstanceState) { grantButton.setOnClickListener(v -> { Intent response = new Intent(getIntent()); - AndroidSharedKeyRecord record = AndroidSharedKeyRecord.generateNewSharingKey(); + AndroidSharedKeyRecord record = null; + try { + record = AndroidSharedKeyRecord.generateNewSharingKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + Toast.makeText(this, Localization.get("app.key.request.encryption.key.error"), Toast.LENGTH_LONG).show(); + Logger.exception("Exception while generating encryption key ", e); + return; + } CommCareApplication.instance().getGlobalStorage(AndroidSharedKeyRecord.class).write(record); record.writeResponseToIntent(response); setResult(AppCompatActivity.RESULT_OK, response); diff --git a/app/src/org/commcare/activities/PostRequestActivity.java b/app/src/org/commcare/activities/PostRequestActivity.java index 82d32563a3..a001f22965 100644 --- a/app/src/org/commcare/activities/PostRequestActivity.java +++ b/app/src/org/commcare/activities/PostRequestActivity.java @@ -19,6 +19,7 @@ import org.commcare.tasks.DataPullTask; import org.commcare.tasks.ModernHttpTask; import org.commcare.tasks.templates.CommCareTaskConnector; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.views.ManagedUi; import org.commcare.views.UiElement; import org.commcare.views.dialogs.CustomProgressDialog; @@ -214,6 +215,11 @@ public void handleIOException(IOException exception) { } } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + enterErrorState(Localization.get("post.cache.encryption.key.error", exception.getMessage())); + } + @Override public void onBackPressed() { if (inErrorState) { diff --git a/app/src/org/commcare/activities/QueryRequestActivity.java b/app/src/org/commcare/activities/QueryRequestActivity.java index c82d51958f..c21d660681 100644 --- a/app/src/org/commcare/activities/QueryRequestActivity.java +++ b/app/src/org/commcare/activities/QueryRequestActivity.java @@ -23,6 +23,7 @@ import org.commcare.session.RemoteQuerySessionManager; import org.commcare.tasks.ModernHttpTask; import org.commcare.tasks.templates.CommCareTaskConnector; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.utils.SessionRegistrationHelper; import org.commcare.utils.SessionUnavailableException; import org.commcare.views.UserfacingErrorHandling; @@ -196,6 +197,11 @@ public void handleIOException(IOException exception) { } } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + enterErrorState(Localization.get("post.cache.encryption.key.error", exception.getMessage())); + } + @Override public CustomProgressDialog generateProgressDialog(int taskId) { String title, message; diff --git a/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java b/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java index 4e62fbc811..1487090e20 100755 --- a/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java +++ b/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java @@ -9,6 +9,7 @@ import org.commcare.models.framework.Persisting; import org.commcare.modern.database.Table; import org.commcare.modern.models.MetaField; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.services.Logger; import org.javarosa.core.util.PropertyUtils; @@ -48,7 +49,8 @@ public AndroidSharedKeyRecord(String keyId, byte[] privateKey, byte[] publicKey) this.publicKey = publicKey; } - public static AndroidSharedKeyRecord generateNewSharingKey() { + public static AndroidSharedKeyRecord generateNewSharingKey() + throws EncryptionKeyHelper.EncryptionKeyException { KeyPair pair = CryptUtil.generateRandomKeyPair(512); byte[] encodedPrivate = pair.getPrivate().getEncoded(); byte[] encodedPublic = pair.getPublic().getEncoded(); diff --git a/app/src/org/commcare/models/database/user/DemoUserBuilder.java b/app/src/org/commcare/models/database/user/DemoUserBuilder.java index 415de2f964..f90cc69317 100644 --- a/app/src/org/commcare/models/database/user/DemoUserBuilder.java +++ b/app/src/org/commcare/models/database/user/DemoUserBuilder.java @@ -11,6 +11,7 @@ import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.core.encryption.CryptUtil; import org.commcare.models.encryption.ByteEncrypter; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.model.User; import org.javarosa.core.util.PropertyUtils; @@ -65,8 +66,11 @@ private void createAndWriteKeyRecordAndUser() { int userCount = keyRecordDB.getIDsForValue(UserKeyRecord.META_USERNAME, username).size(); if (userCount == 0) { - SecretKey secretKey = CryptUtil.generateRandomSecretKey(); - if (secretKey == null) { + + SecretKey secretKey; + try { + secretKey = CryptUtil.generateRandomSecretKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { throw new RuntimeException("Error setting up user's encrypted storage"); } randomKey = secretKey.getEncoded(); diff --git a/app/src/org/commcare/network/GetAndParseActor.java b/app/src/org/commcare/network/GetAndParseActor.java index 7f23ab1958..f82948e87d 100644 --- a/app/src/org/commcare/network/GetAndParseActor.java +++ b/app/src/org/commcare/network/GetAndParseActor.java @@ -12,6 +12,7 @@ import org.commcare.core.network.AuthInfo; import org.commcare.core.network.AuthenticationInterceptor; import org.commcare.core.network.ModernHttpRequester; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.services.Logger; @@ -108,6 +109,13 @@ public void handleIOException(IOException exception) { } } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + Logger.log(LogTypes.TYPE_ERROR_ENCRYPTION_KEY, + String.format("Encountered EncryptionKeyException while getting response stream for %s response: %s", + requestName, exception.getMessage())); + } + private void processErrorResponse(int responseCode) { Logger.log(LogTypes.TYPE_ERROR_SERVER_COMMS, String.format("Received error response from %s request: %s", requestName, responseCode)); diff --git a/app/src/org/commcare/network/HttpCalloutTask.java b/app/src/org/commcare/network/HttpCalloutTask.java index 4592aaeddb..cb4d21e543 100755 --- a/app/src/org/commcare/network/HttpCalloutTask.java +++ b/app/src/org/commcare/network/HttpCalloutTask.java @@ -10,6 +10,7 @@ import org.commcare.data.xml.DataModelPullParser; import org.commcare.data.xml.TransactionParserFactory; import org.commcare.tasks.templates.CommCareTask; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.io.StreamsUtil; @@ -43,7 +44,7 @@ public enum HttpCalloutOutcomes { NetworkFailureBadPassword, IncorrectPin, AuthOverHttp, - CaptivePortal + ResponseCacheError, CaptivePortal } private final Context c; @@ -99,6 +100,9 @@ protected HttpCalloutOutcomes doTaskBackground(Object... params) { //This is probably related to local files, actually e.printStackTrace(); outcome = HttpCalloutOutcomes.NetworkFailure; + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + e.printStackTrace(); + outcome = HttpCalloutOutcomes.ResponseCacheError; } //If we needed the callout to succeed and it didn't, return our failure. @@ -131,7 +135,8 @@ protected HttpCalloutOutcomes doSetupTaskBeforeRequest() { protected abstract Response doHttpRequest() throws IOException; - protected HttpCalloutOutcomes doResponseSuccess(Response response) throws IOException { + protected HttpCalloutOutcomes doResponseSuccess(Response response) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { beginResponseHandling(response); InputStream input = cacheResponseOpenHandle(response); @@ -162,7 +167,8 @@ protected HttpCalloutOutcomes doResponseSuccess(Response response) protected abstract TransactionParserFactory getTransactionParserFactory(); - protected InputStream cacheResponseOpenHandle(Response response) throws IOException { + protected InputStream cacheResponseOpenHandle(Response response) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { long dataSizeGuess = ModernHttpRequester.getContentLength(response); BitCache cache = BitCacheFactory.getCache(new AndroidCacheDirSetup(c), dataSizeGuess); diff --git a/app/src/org/commcare/network/RemoteDataPullResponse.java b/app/src/org/commcare/network/RemoteDataPullResponse.java index 6981596917..c6f0336581 100644 --- a/app/src/org/commcare/network/RemoteDataPullResponse.java +++ b/app/src/org/commcare/network/RemoteDataPullResponse.java @@ -7,6 +7,7 @@ import org.commcare.tasks.DataPullTask; import org.commcare.core.network.bitcache.BitCache; import org.commcare.core.network.bitcache.BitCacheFactory; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.io.StreamsUtil; @@ -49,7 +50,8 @@ protected RemoteDataPullResponse(DataPullTask task, * * @throws IOException If there is an issue reading or writing the response. */ - public BitCache writeResponseToCache(Context c) throws IOException { + public BitCache writeResponseToCache(Context c) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { BitCache cache = null; try (InputStream input = getInputStream()) { final long dataSizeGuess = ModernHttpRequester.getContentLength(response); diff --git a/app/src/org/commcare/tasks/AsyncRestoreHelper.java b/app/src/org/commcare/tasks/AsyncRestoreHelper.java index 555c33ac2f..19299907a7 100644 --- a/app/src/org/commcare/tasks/AsyncRestoreHelper.java +++ b/app/src/org/commcare/tasks/AsyncRestoreHelper.java @@ -1,6 +1,7 @@ package org.commcare.tasks; import org.commcare.network.RemoteDataPullResponse; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.services.Logger; @@ -74,7 +75,7 @@ private boolean parseProgressFromRetryResult(RemoteDataPullResponse response) { } eventType = parser.next(); } while (eventType != KXmlParser.END_DOCUMENT); - } catch (IOException | XmlPullParserException e) { + } catch (IOException | XmlPullParserException | EncryptionKeyHelper.EncryptionKeyException e) { Logger.log(LogTypes.TYPE_USER, "Error while parsing progress values of retry result"); } finally { diff --git a/app/src/org/commcare/tasks/DataPullTask.java b/app/src/org/commcare/tasks/DataPullTask.java index 180cb5a04c..57a8257f67 100644 --- a/app/src/org/commcare/tasks/DataPullTask.java +++ b/app/src/org/commcare/tasks/DataPullTask.java @@ -34,6 +34,7 @@ import org.commcare.services.CommCareSessionService; import org.commcare.sync.ExternalDataUpdateHelper; import org.commcare.tasks.templates.CommCareTask; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.FormSaveUtil; import org.commcare.utils.SessionUnavailableException; @@ -223,8 +224,10 @@ private byte[] getEncryptionKey() { private void initUKRForLogin() { if (blockRemoteKeyManagement || shouldGenerateFirstKey()) { - SecretKey newKey = CryptUtil.generateRandomSecretKey(); - if (newKey == null) { + SecretKey newKey = null; + try { + newKey = CryptUtil.generateRandomSecretKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { return; } String sandboxId = PropertyUtils.genUUID().replace("-", ""); @@ -294,6 +297,9 @@ private ResultAndError getRequestResultOrRetry(AndroidTransactio } catch (UnknownSyncError e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to Unknown Error|" + e.getMessage()); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + e.printStackTrace(); + Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to Cache encryption Error|" + e.getMessage()); } wipeLoginIfItOccurred(); @@ -305,8 +311,9 @@ private ResultAndError getRequestResultOrRetry(AndroidTransactio * @return the proper result, or null if we have not yet been able to determine the result to * return */ - private ResultAndError makeRequestAndHandleResponse(AndroidTransactionParserFactory factory) - throws IOException, UnknownSyncError { + private ResultAndError makeRequestAndHandleResponse( + AndroidTransactionParserFactory factory) + throws IOException, UnknownSyncError, EncryptionKeyHelper.EncryptionKeyException { RemoteDataPullResponse pullResponse = dataPullRequester.makeDataPullRequest(this, requestor, server, !loginNeeded, skipFixtures); @@ -352,7 +359,7 @@ private ResultAndError handleAuthFailed() { */ private ResultAndError handleSuccessResponseCode( RemoteDataPullResponse pullResponse, AndroidTransactionParserFactory factory) - throws IOException, UnknownSyncError { + throws IOException, UnknownSyncError, EncryptionKeyHelper.EncryptionKeyException { asyncRestoreHelper.completeServerProgressBarIfShowing(); handleLoginNeededOnSuccess(); @@ -541,6 +548,10 @@ private Pair recover(CommcareRequestEndpoints requestor, Androi //Ok, well, we're bailing here, but we didn't make any changes Logger.log(LogTypes.TYPE_USER, "Sync Recovery Failed due to IOException|" + e.getMessage()); return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, ""); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + e.printStackTrace(); + Logger.log(LogTypes.TYPE_USER, "Sync Recovery Failed due to Cache encryption error|" + e.getMessage()); + return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, ""); } this.publishProgress(PROGRESS_RECOVERY_STARTED); diff --git a/app/src/org/commcare/tasks/ModernHttpTask.java b/app/src/org/commcare/tasks/ModernHttpTask.java index 20d87623da..ce8cf425ab 100644 --- a/app/src/org/commcare/tasks/ModernHttpTask.java +++ b/app/src/org/commcare/tasks/ModernHttpTask.java @@ -12,9 +12,11 @@ import org.commcare.core.network.HTTPMethod; import org.commcare.core.network.ModernHttpRequester; import org.commcare.tasks.templates.CommCareTask; +import org.commcare.util.EncryptionKeyHelper; import java.io.IOException; import java.io.InputStream; +import java.security.Key; import java.util.HashMap; import javax.annotation.Nullable; @@ -36,7 +38,7 @@ public class ModernHttpTask private final ModernHttpRequester requester; private InputStream responseDataStream; - private IOException mException; + private Exception mException; private Response mResponse; // Use for GET request @@ -72,7 +74,7 @@ protected Void doTaskBackground(Void... params) { if (mResponse.isSuccessful()) { responseDataStream = requester.getResponseStream(mResponse); } - } catch (IOException e) { + } catch (IOException | EncryptionKeyHelper.EncryptionKeyException e) { mException = e; } return null; @@ -83,7 +85,11 @@ protected void deliverResult(HttpResponseProcessor httpResponseProcessor, Void result) { if (mException != null) { - httpResponseProcessor.handleIOException(mException); + if (mException instanceof IOException ioExcep) { + httpResponseProcessor.handleIOException(ioExcep); + } else if (mException instanceof EncryptionKeyHelper.EncryptionKeyException encryptKeyExcep) { + httpResponseProcessor.handleEncryptionKeyException(encryptKeyExcep); + } } else { // route to appropriate callback based on http response code ModernHttpRequester.processResponse( diff --git a/app/src/org/commcare/utils/TemplatePrinterUtils.java b/app/src/org/commcare/utils/TemplatePrinterUtils.java index cf29bee5bf..359bdb0c3a 100755 --- a/app/src/org/commcare/utils/TemplatePrinterUtils.java +++ b/app/src/org/commcare/utils/TemplatePrinterUtils.java @@ -6,6 +6,7 @@ import org.commcare.android.javarosa.IntentCallout; import org.commcare.core.encryption.CryptUtil; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.views.dialogs.StandardAlertDialog; import java.io.BufferedReader; @@ -38,7 +39,18 @@ public abstract class TemplatePrinterUtils { private static final String FORMAT_REGEX_WITH_DELIMITER = "((?<=%2$s)|(?=%1$s))"; - private static final SecretKey KEY = CryptUtil.generateRandomSecretKey(); + private static final SecretKey KEY; + + static { + SecretKey secretKey = null; + try { + secretKey = CryptUtil.generateRandomSecretKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + secretKey = null; + } finally{ + KEY = secretKey; + } + } /** * Concatenate all Strings in a String array to one String. diff --git a/app/unit-tests/src/org/commcare/CommCareTestApplication.java b/app/unit-tests/src/org/commcare/CommCareTestApplication.java index 76cc4d8f48..70240c0446 100644 --- a/app/unit-tests/src/org/commcare/CommCareTestApplication.java +++ b/app/unit-tests/src/org/commcare/CommCareTestApplication.java @@ -26,6 +26,7 @@ import org.commcare.network.DataPullRequester; import org.commcare.network.LocalReferencePullResponseFactory; import org.commcare.services.CommCareSessionService; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.model.User; import org.javarosa.core.reference.ReferenceManager; @@ -211,7 +212,11 @@ public void startUserSession(byte[] symetricKey, UserKeyRecord record, boolean r } if (user != null) { user.setCachedPwd(cachedUserPassword); - user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateRandomSecretKey().getEncoded(), cachedUserPassword)); + try { + user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateRandomSecretKey().getEncoded(), cachedUserPassword)); + } catch (EncryptionKeyHelper.EncryptionKeyException e){ + throw new RuntimeException(e); + } } ccService.startSession(user, record); CommCareApplication.instance().setTestingService(ccService); From 403fff490940ea6e278b75c3002ef70d3eec8020 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 24 Jan 2024 18:57:37 +0200 Subject: [PATCH 42/42] Lint --- .../utils/EncryptCredentialsInMemoryTest.kt | 2 +- .../utils/TestKeyStoreEncryptionKeyProvider.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt index bb68181acb..0e01983fa4 100644 --- a/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt +++ b/app/unit-tests/src/org/commcare/utils/EncryptCredentialsInMemoryTest.kt @@ -4,9 +4,9 @@ import android.security.keystore.KeyGenParameterSpec import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.every import io.mockk.mockk +import org.commcare.android.util.TestAppInstaller import org.commcare.CommCareApplication import org.commcare.CommCareTestApplication -import org.commcare.android.util.TestAppInstaller import org.commcare.util.EncryptionKeyHelper import org.commcare.util.EncryptionHelper import org.javarosa.core.model.User diff --git a/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java b/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java index bd6bca8a1d..4387cf4440 100644 --- a/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java +++ b/app/unit-tests/src/org/commcare/utils/TestKeyStoreEncryptionKeyProvider.java @@ -2,15 +2,12 @@ import android.os.Build; import android.security.keystore.KeyProperties; - +import androidx.annotation.RequiresApi; import org.commcare.util.EncryptionHelper; import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.IKeyStoreEncryptionKeyProvider; - import java.security.Key; -import androidx.annotation.RequiresApi; - /** * Class for providing encryption keys backed by Android Keystore for Unit testing * @@ -20,17 +17,20 @@ public class TestKeyStoreEncryptionKeyProvider implements IKeyStoreEncryptionKey @RequiresApi(api = Build.VERSION_CODES.M) private static final String ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; + @RequiresApi(api = Build.VERSION_CODES.M) private static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; + @RequiresApi(api = Build.VERSION_CODES.M) private static final String PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; // Generates a cryptrographic key and adds it to the Android KeyStore @Override - public Key generateCryptographicKeyInKeyStore(String keyAlias, - EncryptionHelper.CryptographicOperation cryptographicOperation) + public Key generateCryptographicKeyInKeyStore( + String keyAlias, EncryptionHelper.CryptographicOperation cryptographicOperation) throws EncryptionKeyHelper.EncryptionKeyException { - throw new EncryptionKeyHelper.EncryptionKeyException("KeyStore encryption key generator provider for testing only"); + throw new EncryptionKeyHelper.EncryptionKeyException( + "KeyStore encryption key generator provider for testing only"); } @Override