Skip to content

Instantly share code, notes, and snippets.

@OleksandrKucherenko
Created February 7, 2020 13:11
Show Gist options
  • Save OleksandrKucherenko/34b5c72445bcf9d5519af6898afcb8fb to your computer and use it in GitHub Desktop.
Save OleksandrKucherenko/34b5c72445bcf9d5519af6898afcb8fb to your computer and use it in GitHub Desktop.
package com.oblador.keychain;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager;
import androidx.test.core.app.ApplicationProvider;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.oblador.keychain.KeychainModule.AccessControl;
import com.oblador.keychain.KeychainModule.Errors;
import com.oblador.keychain.KeychainModule.KnownCiphers;
import com.oblador.keychain.KeychainModule.Maps;
import com.oblador.keychain.cipherStorage.CipherStorage;
import com.oblador.keychain.cipherStorage.CipherStorageBase;
import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAesCbc;
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.KeyStoreAccessException;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.junit.VerificationCollector;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.security.KeyStore;
import java.security.Security;
import javax.crypto.Cipher;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
@RunWith(RobolectricTestRunner.class)
public class KeychainModuleTests {
public static final byte[] BYTES_USERNAME = "username".getBytes();
public static final byte[] BYTES_PASSWORD = "password".getBytes();
/**
* Cancel test after 5 seconds.
*/
@ClassRule
public static Timeout timeout = Timeout.seconds(10);
/**
* Get test method name.
*/
@Rule
public TestName methodName = new TestName();
/**
* Mock all the dependencies.
*/
@Rule
public MockitoRule mockDependencies = MockitoJUnit.rule().silent();
@Rule
public VerificationCollector collector = MockitoJUnit.collector();
/**
* Security fake provider.
*/
private FakeProvider provider = new FakeProvider();
@Before
public void setUp() throws Exception {
provider.configuration.clear();
Security.insertProviderAt(provider, 0);
}
@After
public void tearDown() throws Exception {
Security.removeProvider(FakeProvider.NAME);
}
@NonNull
private ReactApplicationContext getRNContext() {
return new ReactApplicationContext(ApplicationProvider.getApplicationContext());
}
@Test
@Config(sdk = Build.VERSION_CODES.LOLLIPOP)
public void testFingerprintNoHardware_api21() throws Exception {
// GIVEN: API21 android version
ReactApplicationContext context = getRNContext();
KeychainModule module = new KeychainModule(context);
// WHEN: verify availability
final int result = BiometricManager.from(context).canAuthenticate();
final boolean isFingerprintAvailable = module.isFingerprintAuthAvailable();
// THEN: in api lower 23 - biometric is not available at all
assertThat(isFingerprintAvailable, is(false));
assertThat(result, is(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE));
// fingerprint hardware not available, minimal API for fingerprint is api23, Android 6.0
// https://developer.android.com/about/versions/marshmallow/android-6.0
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void testFingerprintAvailableButNotConfigured_api23() throws Exception {
// GIVEN:
// fingerprint api available but not configured properly
// API23 android version
ReactApplicationContext context = getRNContext();
KeychainModule module = new KeychainModule(context);
// set that hardware is available
FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
shadowOf(fm).setIsHardwareDetected(true);
// WHEN: check availability
final int result = BiometricManager.from(context).canAuthenticate();
final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
// THEN: another status from biometric api, fingerprint is still unavailable
assertThat(result, is(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED));
assertThat(isFingerprintWorking, is(false));
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void testFingerprintConfigured_api23() throws Exception {
// GIVEN:
// API23 android version
// Fingerprints are configured
// fingerprint feature is ignored by android os
ReactApplicationContext context = getRNContext();
// set that hardware is available
FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
shadowOf(fm).setIsHardwareDetected(true);
shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available
// WHEN: check availability
final int result = BiometricManager.from(context).canAuthenticate();
final KeychainModule module = new KeychainModule(context);
final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
// THEN: biometric works
assertThat(result, is(BiometricManager.BIOMETRIC_SUCCESS));
assertThat(isFingerprintWorking, is(true));
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
public void testFingerprintConfigured_api28() throws Exception {
// GIVEN:
// API28 android version
// for api24+ system feature should be enabled
// fingerprints are configured
ReactApplicationContext context = getRNContext();
shadowOf(context.getPackageManager()).setSystemFeature(PackageManager.FEATURE_FINGERPRINT, true);
// set that hardware is available
FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
shadowOf(fm).setIsHardwareDetected(true);
shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available
// WHEN: verify availability
final int result = BiometricManager.from(context).canAuthenticate();
final KeychainModule module = new KeychainModule(context);
final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
// THEN: biometrics works
assertThat(result, is(BiometricManager.BIOMETRIC_SUCCESS));
assertThat(isFingerprintWorking, is(true));
}
@Test
@Config(sdk = Build.VERSION_CODES.KITKAT)
public void testExtractFacebookConceal_NoHardware_api19() throws Exception {
// GIVEN:
// API19, minimal Android version
final ReactApplicationContext context = getRNContext();
// WHEN: ask keychain for secured storage
final KeychainModule module = new KeychainModule(context);
final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();
// THEN: expected Facebook cipher storage, its the only one that supports API19
assertThat(storage, notNullValue());
assertThat(storage, instanceOf(CipherStorageFacebookConceal.class));
assertThat(storage.isBiometrySupported(), is(false));
assertThat(storage.securityLevel(), is(SecurityLevel.ANY));
assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.JELLY_BEAN));
assertThat(storage.supportsSecureHardware(), is(false));
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void testExtractAesCbc_NoFingerprintConfigured_api23() throws Exception {
// GIVEN:
// API23 android version
final ReactApplicationContext context = getRNContext();
// WHEN: get the best secured storage
final KeychainModule module = new KeychainModule(context);
final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();
// THEN:
// expected AES cipher storage due no fingerprint available
// AES win and returned instead of facebook cipher
assertThat(storage, notNullValue());
assertThat(storage, instanceOf(CipherStorageKeystoreAesCbc.class));
assertThat(storage.isBiometrySupported(), is(false));
assertThat(storage.securityLevel(), is(SecurityLevel.SECURE_HARDWARE));
assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.M));
assertThat(storage.supportsSecureHardware(), is(true));
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void testExtractRsaEcb_EnabledFingerprint_api23() throws Exception {
// GIVEN:
// API23 android version
// fingerprints configured
final ReactApplicationContext context = getRNContext();
// set that hardware is available and fingerprints configured
final FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
shadowOf(fm).setIsHardwareDetected(true);
shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available
// WHEN: fingerprint availability influence on storage selection
final KeychainModule module = new KeychainModule(context);
final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();
// THEN: expected RsaEcb with working fingerprint
assertThat(isFingerprintWorking, is(true));
assertThat(storage, notNullValue());
assertThat(storage, instanceOf(CipherStorageKeystoreRsaEcb.class));
assertThat(storage.isBiometrySupported(), is(true));
assertThat(storage.securityLevel(), is(SecurityLevel.SECURE_HARDWARE));
assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.M));
assertThat(storage.supportsSecureHardware(), is(true));
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
public void testExtractRsaEcb_EnabledFingerprint_api28() throws Exception {
// GIVEN:
// API28 android version
// fingerprint feature enabled
// fingerprints configured
final ReactApplicationContext context = getRNContext();
shadowOf(context.getPackageManager()).setSystemFeature(PackageManager.FEATURE_FINGERPRINT, true);
// set that hardware is available and fingerprints configured
final FingerprintManager fm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
shadowOf(fm).setIsHardwareDetected(true);
shadowOf(fm).setDefaultFingerprints(5); // 5 fingerprints are available
// WHEN: get secured storage
final int result = BiometricManager.from(context).canAuthenticate();
final KeychainModule module = new KeychainModule(context);
final boolean isFingerprintWorking = module.isFingerprintAuthAvailable();
final CipherStorage storage = module.getCipherStorageForCurrentAPILevel();
// THEN: expected RsaEcb with working fingerprint
assertThat(isFingerprintWorking, is(true));
assertThat(result, is(BiometricManager.BIOMETRIC_SUCCESS));
assertThat(storage, notNullValue());
assertThat(storage, instanceOf(CipherStorageKeystoreRsaEcb.class));
assertThat(storage.isBiometrySupported(), is(true));
assertThat(storage.securityLevel(), is(SecurityLevel.SECURE_HARDWARE));
assertThat(storage.getMinSupportedApiLevel(), is(Build.VERSION_CODES.M));
assertThat(storage.supportsSecureHardware(), is(true));
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void testMigrateStorageFromOlder_api23() throws Exception {
// GIVEN:
final ReactApplicationContext context = getRNContext();
final CipherStorage aes = Mockito.mock(CipherStorage.class);
final CipherStorage rsa = Mockito.mock(CipherStorage.class);
final CipherStorage.DecryptionResult decrypted = new CipherStorage.DecryptionResult("user", "password");
final CipherStorage.EncryptionResult encrypted = new CipherStorage.EncryptionResult("user".getBytes(), "password".getBytes(), rsa);
final KeychainModule module = new KeychainModule(context);
final SharedPreferences prefs = context.getSharedPreferences(PrefsStorage.KEYCHAIN_DATA, Context.MODE_PRIVATE);
when(
rsa.encrypt(eq("dummy"), eq("user"), eq("password"), any())
).thenReturn(encrypted);
when(rsa.getCipherStorageName()).thenReturn("dummy");
// WHEN:
module.migrateCipherStorage("dummy", rsa, aes, decrypted);
final String username = prefs.getString(PrefsStorage.getKeyForUsername("dummy"), "");
final String password = prefs.getString(PrefsStorage.getKeyForPassword("dummy"), "");
final String cipherName = prefs.getString(PrefsStorage.getKeyForCipherStorage("dummy"), "");
// THEN:
// delete of key from old storage
// re-store of encrypted data in shared preferences
verify(rsa).encrypt("dummy", "user", "password", SecurityLevel.ANY);
verify(aes).removeKey("dummy");
// Base64.DEFAULT force '\n' char in the end of string
assertThat(username, is("dXNlcg==\n"));
assertThat(password, is("cGFzc3dvcmQ=\n"));
assertThat(cipherName, is("dummy"));
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
public void testGetSecurityLevel_Unspecified_api28() throws Exception {
// GIVE:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final Promise mockPromise = mock(Promise.class);
// WHEN:
module.getSecurityLevel(null, mockPromise);
// THEN:
verify(mockPromise).resolve(SecurityLevel.SECURE_HARDWARE.name());
}
@Test
@Config(sdk = Build.VERSION_CODES.M)
public void testGetSecurityLevel_Unspecified_api23() throws Exception {
// GIVE:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final Promise mockPromise = mock(Promise.class);
// WHEN:
module.getSecurityLevel(null, mockPromise);
// THEN:
verify(mockPromise).resolve(SecurityLevel.SECURE_HARDWARE.name());
}
@Test
@Config(sdk = Build.VERSION_CODES.LOLLIPOP)
public void testGetSecurityLevel_Unspecified_api21() throws Exception {
// GIVE:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final Promise mockPromise = mock(Promise.class);
// WHEN:
module.getSecurityLevel(null, mockPromise);
// THEN:
verify(mockPromise).resolve(SecurityLevel.ANY.name());
}
@Test
@Config(sdk = Build.VERSION_CODES.KITKAT)
public void testGetSecurityLevel_Unspecified_api19() throws Exception {
// GIVE:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final Promise mockPromise = mock(Promise.class);
// WHEN:
module.getSecurityLevel(null, mockPromise);
// THEN:
verify(mockPromise).resolve(SecurityLevel.ANY.name());
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
public void testGetSecurityLevel_NoBiometry_api28() throws Exception {
// GIVE:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final Promise mockPromise = mock(Promise.class);
// WHEN:
final JavaOnlyMap options = new JavaOnlyMap();
options.putString(Maps.ACCESS_CONTROL, AccessControl.DEVICE_PASSCODE);
module.getSecurityLevel(options, mockPromise);
// THEN:
verify(mockPromise).resolve(SecurityLevel.SECURE_HARDWARE.name());
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
public void testGetSecurityLevel_NoBiometry_NoSecuredHardware_api28() throws Exception {
// GIVE:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final Promise mockPromise = mock(Promise.class);
// set key info - software method
provider.configuration.put("isInsideSecureHardware", false);
// WHEN:
final JavaOnlyMap options = new JavaOnlyMap();
options.putString(Maps.ACCESS_CONTROL, AccessControl.DEVICE_PASSCODE);
module.getSecurityLevel(options, mockPromise);
// THEN:
// expected AesCbc usage
assertThat(provider.mocks.get("KeyGenerator"), notNullValue());
assertThat(provider.mocks.get("KeyGenerator").get("AES"), notNullValue());
assertThat(provider.mocks.get("KeyPairGenerator"), notNullValue());
assertThat(provider.mocks.get("KeyPairGenerator").get("RSA"), notNullValue());
verify(mockPromise).resolve(SecurityLevel.SECURE_SOFTWARE.name());
}
@Test
@Config(sdk = Build.VERSION_CODES.P)
public void testDowngradeBiometricToAes_api28() throws Exception {
// GIVEN:
final ReactApplicationContext context = getRNContext();
final KeychainModule module = new KeychainModule(context);
final PrefsStorage prefs = new PrefsStorage(context);
final Cipher mockCipher = Mockito.mock(Cipher.class);
final KeyStore mockKeyStore = Mockito.mock(KeyStore.class);
final CipherStorage storage = module.getCipherStorageByName(KnownCiphers.RSA);
final CipherStorage.EncryptionResult result = new CipherStorage.EncryptionResult(BYTES_USERNAME, BYTES_PASSWORD, storage);
final Promise mockPromise = mock(Promise.class);
final JavaOnlyMap options = new JavaOnlyMap();
// store record done with RSA/Biometric cipher
prefs.storeEncryptedEntry("dummy", result);
assertThat(storage, instanceOf(CipherStorage.class));
((CipherStorageBase)storage).setCipher(mockCipher).setKeyStore(mockKeyStore);
when(mockKeyStore.getKey(eq("dummy"), isNull())).thenReturn(null); // return empty Key!
// WHEN:
module.getGenericPasswordForOptions("dummy", options, mockPromise);
// THEN:
ArgumentCaptor<Exception> exception = ArgumentCaptor.forClass(Exception.class);
verify(mockPromise).reject(eq(Errors.E_CRYPTO_FAILED), exception.capture());
assertThat(exception.getValue(), instanceOf(CryptoFailedException.class));
assertThat(exception.getValue().getCause(), instanceOf(KeyStoreAccessException.class));
assertThat(exception.getValue().getMessage(), is("Wrapped error: Empty key extracted!"));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment