Skip to content

Commit

Permalink
Android: Added missing inline documentation and improved tests #354
Browse files Browse the repository at this point in the history
  • Loading branch information
hvge committed Mar 4, 2021
1 parent 0ff8c94 commit 21d846a
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 3 deletions.
Expand Up @@ -47,10 +47,10 @@ public void setUp() {
@Test
public void testCachedKeychains() throws Exception {
final Keychain keychain1_a = KeychainFactory.getKeychain(androidContext, KEYCHAIN_1_NAME, KeychainProtection.NONE);
final Keychain keychain1_b = KeychainFactory.getKeychain(androidContext, KEYCHAIN_1_NAME, KeychainProtection.NONE);
assertEquals(keychain1_a, keychain1_b);
final Keychain keychain2_a = KeychainFactory.getKeychain(androidContext, KEYCHAIN_2_NAME, KeychainProtection.NONE);
final Keychain keychain1_b = KeychainFactory.getKeychain(androidContext, KEYCHAIN_1_NAME, KeychainProtection.NONE);
final Keychain keychain2_b = KeychainFactory.getKeychain(androidContext, KEYCHAIN_2_NAME, KeychainProtection.NONE);
assertEquals(keychain1_a, keychain1_b);
assertEquals(keychain2_a, keychain2_b);
}

Expand Down
Expand Up @@ -17,17 +17,22 @@
package io.getlime.security.powerauth.keychain;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import javax.crypto.SecretKey;

import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import io.getlime.security.powerauth.exception.PowerAuthErrorCodes;
import io.getlime.security.powerauth.exception.PowerAuthErrorException;
import io.getlime.security.powerauth.keychain.impl.AesGcmImpl;
import io.getlime.security.powerauth.keychain.impl.BaseKeychainTest;
import io.getlime.security.powerauth.keychain.impl.DefaultStrongBoxSupport;
import io.getlime.security.powerauth.keychain.impl.EncryptedKeychain;
Expand Down Expand Up @@ -147,6 +152,8 @@ public void testStrongBoxEnabledDisabled() throws Exception {
// Now acquire some keychain.
Keychain k1 = KeychainFactory.getKeychain(androidContext, KEYCHAIN_NAME1, KeychainProtection.NONE);
assertNotNull(k1);
fillTestValues(k1);
verifyEncryptedData(KEYCHAIN_NAME1, false, "test.string_NotEmpty");

try {
// The following statement must fail, even when there's no change in configuration.
Expand Down Expand Up @@ -175,6 +182,8 @@ public void testStrongBoxEnabledDisabled() throws Exception {
// Now acquire some keychain.
k1 = KeychainFactory.getKeychain(androidContext, KEYCHAIN_NAME1, KeychainProtection.NONE);
assertNotNull(k1);
testFilledValues(k1, false);
verifyEncryptedData(KEYCHAIN_NAME1, true, "test.string_NotEmpty");

try {
// The following statement must fail, even when there's no change in configuration.
Expand Down Expand Up @@ -231,6 +240,9 @@ public void testMigrationFromV0toStrongBoxNotSupported() throws Exception {
// Empty string is treated as null after migration.
runAllStandardValidations(k1, true);
runAllStandardValidations(k2, true);

verifyEncryptedData(KEYCHAIN_NAME1, true, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, true, "test.data_NotEmpty");
}

@Test
Expand Down Expand Up @@ -258,6 +270,9 @@ public void testMigrationFromV0toStrongBoxDisabled() throws Exception {
// Empty string is treated as null after migration.
runAllStandardValidations(k1, true);
runAllStandardValidations(k2, true);

verifyEncryptedData(KEYCHAIN_NAME1, false, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, false, "test.data_NotEmpty");
}

@Test
Expand Down Expand Up @@ -291,6 +306,9 @@ public void testMigrationFromV0toStrongBoxEnabled() throws Exception {
// Empty string is treated as null after migration.
runAllStandardValidations(k1, true);
runAllStandardValidations(k2, true);

verifyEncryptedData(KEYCHAIN_NAME1, true, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, true, "test.data_NotEmpty");
}

@Test
Expand Down Expand Up @@ -345,6 +363,9 @@ public void testStrongBoxSupportChange() throws Exception {
fillTestValues(k1);
fillTestValues(k2);

verifyEncryptedData(KEYCHAIN_NAME1, false, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, false, "test.data_NotEmpty");

// Now enable StrongBox support again
KeychainFactory.setStrongBoxSupport(new FakeStrongBoxSupport(true, true));
// KeychainFactory did reset its cache, so we need to acquire keychains again.
Expand All @@ -361,6 +382,9 @@ public void testStrongBoxSupportChange() throws Exception {
// Validate test values again
runAllStandardValidations(k1, false);
runAllStandardValidations(k2, false);

verifyEncryptedData(KEYCHAIN_NAME1, true, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, true, "test.data_NotEmpty");
}

@Test
Expand Down Expand Up @@ -392,6 +416,9 @@ public void testStrongBoxSupportChangeFromV1() throws Exception {
downgradeKeychainToV1(KEYCHAIN_NAME1);
downgradeKeychainToV1(KEYCHAIN_NAME2);

verifyEncryptedData(KEYCHAIN_NAME1, true, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, true, "test.data_NotEmpty");

// We have V1 data prepared, so now we can simulate situation when SDK determine that
// StrongBox is not reliable.
KeychainFactory.setStrongBoxSupport(new FakeStrongBoxSupport(true, false));
Expand All @@ -410,6 +437,9 @@ public void testStrongBoxSupportChangeFromV1() throws Exception {
// Validate stored values
runAllStandardValidations(k1, false);
runAllStandardValidations(k2, false);

verifyEncryptedData(KEYCHAIN_NAME1, false, "test.string_NotEmpty");
verifyEncryptedData(KEYCHAIN_NAME2, false, "test.data_NotEmpty");
}

/**
Expand Down Expand Up @@ -448,4 +478,37 @@ void eraseAllKeychainData(@NonNull String identifier) {
.clear()
.apply();
}

/**
* Verify whether there's value stored in keychain and encrypted with the right key.
*
* @param keychainIdentifier Keychain identifier.
* @param primaryKey Use primary or backup key.
* @param dataKey Data to be verified.
*/
void verifyEncryptedData(@NonNull String keychainIdentifier, boolean primaryKey, @NonNull String dataKey) {
final String keyIdentifier = primaryKey ? MASTER_KEY_ALIAS : MASTER_BACK_KEY_ALIAS;

// Get raw data from keychain
final SharedPreferences preferences = androidContext.getSharedPreferences(keychainIdentifier, Context.MODE_PRIVATE);
assertNotNull(preferences);
final String encodedEncryptedData = preferences.getString(dataKey, null);
assertNotNull(encodedEncryptedData);

// Decode from Base64
final byte[] encryptedData = Base64.decode(encodedEncryptedData, Base64.NO_WRAP);
assertNotNull(encryptedData);
assertTrue(encryptedData.length > 0);

// Acquire secret key
final SymmetricKeyProvider keyProvider = SymmetricKeyProvider.getAesGcmKeyProvider(keyIdentifier, primaryKey, realStrongBoxSupport, 256, true, null);
assertNotNull(keyProvider);
assertTrue(keyProvider.containsSecretKey());
final SecretKey secretKey = keyProvider.getOrCreateSecretKey(androidContext, false);
assertNotNull(secretKey);

// Try decrypt data
final byte[] plainData = AesGcmImpl.decrypt(encryptedData, secretKey, keychainIdentifier);
assertNotNull(plainData);
}
}
Expand Up @@ -528,7 +528,14 @@ private void putVersion(@NonNull SharedPreferences.Editor editor) {
*/
public static final int STRONGBOX_DISABLED = 3;


/**
* Compare the current StrongBox support against the value stored in the shared preferences and
* re-encrypt keychain content if needed. The function also upgrade keychain version from V1
* to V2, if possible. In case of failure, function remove all data from keychain.
*
* @param preferences Underlying {@code SharedPreferences} that contains content of keychain.
* @return {@code true} in case of success.
*/
public boolean updateStrongBoxSupport(@NonNull SharedPreferences preferences) {
// Determine keychain version
final int keychainVersion = preferences.getInt(ENCRYPTED_KEYCHAIN_VERSION_KEY, KEYCHAIN_V0);
Expand Down Expand Up @@ -567,10 +574,12 @@ public boolean updateStrongBoxSupport(@NonNull SharedPreferences preferences) {
if (strongBoxEnabled) {
// If StrongBox is enabled, then we have to re-encrypt data from the backup key
// to the regular one.
PA2Log.d("EncryptedKeychain: " + identifier + ": Re-encrypting data with StrongBox backed key.");
sourceKey = backupKeyProvider.getOrCreateSecretKey(context, false);
destinationKey = regularKeyProvider.getOrCreateSecretKey(context, false);
} else {
// StrongBox is disabled, so we have to re-encrypt data from the regular key to the backup one.
PA2Log.d("EncryptedKeychain: " + identifier + ": Re-encrypting data with regular key.");
sourceKey = regularKeyProvider.getOrCreateSecretKey(context, false);
destinationKey = backupKeyProvider.getOrCreateSecretKey(context, false);
}
Expand Down Expand Up @@ -601,6 +610,14 @@ public boolean updateStrongBoxSupport(@NonNull SharedPreferences preferences) {
return result;
}

/**
* Re-encrypt content of keychain to a different encryption key.
*
* @param preferences {@link SharedPreferences} containing keychain data.
* @param source {@link SecretKey} to decrypt data.
* @param destination {@link SecretKey} to encrypt data.
* @return {@code true} in case of success.
*/
private boolean reEncryptKeychain(@NonNull SharedPreferences preferences, @NonNull SecretKey source, @NonNull SecretKey destination) {
boolean result = true;
// Prepare hash map for decrypted content.
Expand Down

0 comments on commit 21d846a

Please sign in to comment.