Android Keystore & Biometria - cz. II [PL]
Android Keystore & Biometria
Bezpieczeństwo danych na skompromitowanym urządzeniu
(część II)
W pierwszym artykule zbadaliśmy stopień trudności uzyskania przez intruza dostępu do kluczy szyfrujących przechowywanych w Android Keystore.
Wnioski nie pozostawiają złudzeń, intruz z wysokimi uprawnieniami może wykorzystać dowolny klucz dowolnej aplikacji, nawet w przypadku rozwiązań wspomaganych sprzętowo (TEE i StrongBox).
W tym artykule, sprawdzimy jak uwierzytelnianie blokadą ekranu lub biometrią, może pomóc w ochronie dostępu do kluczy.
Przyjrzymy się też ciekawemu przypadkowi z drobnym błędem w konfiguracji, który może zniweczyć wysiłek włożony w implementację ochrony z uwierzytelnianiem.
Uwierzytelnianie w Android Keystore
Kryterium uwierzytelniania należy zdefiniować już podczas generowania klucza w Android Keystore.
Aby to zrobić, musimy odwołać się do funkcji setUserAuthenticationRequired(true).
[https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)]
Ponadto, mamy do dyspozycji kilka funkcji pozwalających ustawić dodatkowe parametry:
- setUserAuthenticationParameters(int timeout, int type) - określa czas na jaki klucz zostanie odblokowany oraz wymagany typ uwierzytelniania (blokada ekranu lub biometria)
- setUserAuthenticationValidWhileOnBody(boolean remainsValid) - określa czy czasowe uwierzytelnianie powinno zostać unieważnione, gdy sensor ciała wykryje rozdzielenie fizycznego kontaktu urządzenia z użytkownikiem
- setInvalidatedByBiometricEnrollment(boolean invalidateKey) - określa czy klucz powinien zostać unieważniony wraz ze zmianą ustawień biometrii
Po wygenerowaniu klucza, musimy obsłużyć całą procedurę uwierzytelnienia przy pomocy klasy BiometricPrompt.
Przykład poprawnego użycia możemy znaleźć w udostępnionym dla deweloperów tutorialu: https://developer.android.com/training/sign-in/biometric-auth
Mała uwaga: klasa BiometricPrompt posiada dwie oficjalne implementacje, które nieco różnią się między sobą:
1. androidx.biometric.BiometricPrompt
[https://developer.android.com/reference/androidx/biometric/BiometricPrompt]
2. android.hardware.biometrics.BiometricPrompt
[https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt]
Podczas testów, wykorzystamy implementację z biblioteki androidx.biometric i w tym celu dodajemy odpowiedni wpis w konfiguracji projektu Android Studio.
build.gradle
[...]
dependencies {
implementation 'androidx.biometric:biometric:1.1.0'
[...]
Bazowa aplikacja
Skorzystamy z bazowej aplikacji z pierwszej części artykułu.
W przykładach dla TEE i StrongBox podążaliśmy za zaleceniami najlepszych praktyk bezpieczeństwa, implementując rozwiązanie z EncryptedFile i MasterKeys.
W bieżących przykładach nie możemy skorzystać z tych rozwiązań, z kilku powodów:
- klasa EncryptedFile zaimplementowana jest przy użyciu wieloplatformowej biblioteki Google Tink, która nie przewiduje specyficznego dla systemu Android uwierzytelniania
- nie powinniśmy wymuszać uwierzytelniania na kluczu, który domyślnie wykorzystywany jest przez komponenty nieobsługujące uwierzytelniania
- klasa MasterKeys nie pozwala na wygenerowanie klucza wymagającego uwierzytelniania przy każdym użyciu
if (spec.isUserAuthenticationRequired()
&& spec.getUserAuthenticationValidityDurationSeconds() < 1) {
throw new IllegalArgumentException(
"per-operation authentication is not supported "
+ "(UserAuthenticationValidityDurationSeconds must be >0)");
}
W związku z powyższym:
- wygenerujemy klucz z własnym aliasem
- napiszemy implementację zbliżoną do EncrptedFile, z tą różnicą, że ze względu na mały rozmiar pliku, zaimplementujemy prosty AEAD zamiast Streaming AEAD
Nowy alias klucza: file_encryption_master_key
Klasa SecureFileBiometricImpl implementuje SecureFile do obsługi pliku zaszyfrowanego algorytmem AES-256-GCM (AEAD), dla którego klucz uwierzytelniany jest poprzez BiometricPrompt. Treść i szczegóły wyświetlanego komunikatu przekazywane są w konstruktorze jako parametr typu BiometricPrompt.PromptInfo.
SecureFileBiometricImpl.java
package pl.isec.baseapp.devicecredential;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.security.KeyException;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public final class SecureFileBiometricImpl extends SecureFile {
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String AUTHENTICATION_FAILED = "Authentication failed";
private static final String AES_GCM_NOPADDING = "AES/GCM/NoPadding";
private static final int IV_SIZE_IN_BYTES = 12;
private static final int TAG_SIZE_IN_BYTES = 16;
private final BiometricPrompt.PromptInfo mPromptInfo;
private final FragmentActivity mActivity;
public SecureFileBiometricImpl(
@NonNull File file,
@NonNull Context context,
@NonNull String keyAlias,
@NonNull FragmentActivity activity,
@NonNull BiometricPrompt.PromptInfo promptInfo
){
super(file, context, keyAlias);
mActivity = activity;
mPromptInfo = promptInfo;
}
@Override
public void openFileInput(@NonNull OpenFileInputCallback callback){
try {
byte[] iv = new byte[IV_SIZE_IN_BYTES];
InputStream inputStream = Files.newInputStream(mFile.toPath(), READ);
inputStream.read(iv, 0, IV_SIZE_IN_BYTES);
inputStream.close();
Cipher cipher = Cipher.getInstance(AES_GCM_NOPADDING);
GCMParameterSpec spec = new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, iv);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec);
newBiometricPrompt(callback).authenticate(
mPromptInfo,
new BiometricPrompt.CryptoObject(cipher)
);
} catch (Exception e) {
callback.onError(e.getMessage());
}
}
@Override
public void openFileOutput(@NonNull OpenFileOutputCallback callback){
try {
Cipher cipher = Cipher.getInstance(AES_GCM_NOPADDING);
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
newBiometricPrompt(callback).authenticate(
mPromptInfo,
new BiometricPrompt.CryptoObject(cipher)
);
} catch (Exception e) {
callback.onError(e.getMessage());
}
}
private BiometricPrompt newBiometricPrompt(OpenFileInputCallback callback){
return new BiometricPrompt(
mActivity,
ContextCompat.getMainExecutor(mContext),
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
callback.onError(errString);
}
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
byte[] ciphertext = Files.readAllBytes(mFile.toPath());
Cipher cipher = result.getCryptoObject().getCipher();
cipher.updateAAD(mFile.getName().getBytes(UTF_8));
callback.onInputStreamReady(
new ByteArrayInputStream(
cipher.doFinal(
ciphertext,
IV_SIZE_IN_BYTES,
ciphertext.length - IV_SIZE_IN_BYTES
)
)
);
} catch (Exception e){
callback.onError(e.getMessage());
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
callback.onError(AUTHENTICATION_FAILED);
}
});
}
private BiometricPrompt newBiometricPrompt(OpenFileOutputCallback callback){
return new BiometricPrompt(
mActivity,
ContextCompat.getMainExecutor(mContext),
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
callback.onError(errString);
}
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
callback.onOutputStreamReady(outputStream);
byte[] plaintext = outputStream.toByteArray();
Cipher cipher = result.getCryptoObject().getCipher();
byte[] ciphertext = new byte[
IV_SIZE_IN_BYTES + cipher.getOutputSize(plaintext.length)
];
System.arraycopy(cipher.getIV(), 0, ciphertext, 0, IV_SIZE_IN_BYTES);
cipher.updateAAD(mFile.getName().getBytes(UTF_8));
cipher.doFinal(
plaintext,0, plaintext.length,
ciphertext, IV_SIZE_IN_BYTES
);
Files.write(mFile.toPath(), ciphertext, CREATE, WRITE, TRUNCATE_EXISTING);
} catch (Exception e){
callback.onError(e.getMessage());
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
callback.onError(AUTHENTICATION_FAILED);
}
});
}
private SecretKey getSecretKey() throws IOException, GeneralSecurityException {
KeyStore ks = KeyStore.getInstance(ANDROID_KEYSTORE);
ks.load(null);
if (!ks.containsAlias(mKeyAlias)) {
throw new KeyException("Key alias not found: "+ mKeyAlias);
}
return (SecretKey) ks.getKey(mKeyAlias, null);
}
}
Aplikacja intruza
W pierwszej części artykułu testowaliśmy rozwiązania z poziomu graficznej oraz konsolowej aplikacji intruza.
Wykazaliśmy, że w przypadku graficznej wersji, wystarczy zwykła aplikacja uruchomiona z odpowiednimi uprawnieniami. Jednakże, metoda ta wymaga ponownego uruchomienia środowiska ART, co nie ujdzie uwadze użytkownika.
W tej części artykułu, ograniczymy się do dyskretnej metody z implementacją konsolowej aplikacji intruza, pozbawionej możliwości interakcji z użytkownikiem.
W stosunku do poprzedniej aplikacji, dostosujemy jedynie alias klucza oraz sposób odczytu pliku, aby był zgodny z implementacją SecureFileBiometricImpl.
- Android Studio -> New Project -> No Activity
- Language: Java
- Minimum SDK: API 31: Android 12.0 (S)
Nazwa pakietu: pl.isec.robber.console.securefilebiometricimpl
Run.java
package pl.isec.robber.console.securefilebiometricimpl;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.READ;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.security.KeyException;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public class Run {
private static final String MASTER_KEY_ALIAS = "file_encryption_master_key";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String AES_GCM_NOPADDING = "AES/GCM/NoPadding";
private static final int IV_SIZE_IN_BYTES = 12;
private static final int TAG_SIZE_IN_BYTES = 16;
public static void main(String args[]){
if(args.length != 2){
System.err.println("Usage:\n\tpl.isec.robber.console.securefileimpl.Run <packageName> <encryptedFileName>");
System.exit(1);
}
String packageName = args[0];
String fileName = args[1];
try {
/** Initialize Android Keystore and application context **/
initAndroidKeystore();
Context context = initAppContext(packageName);
/** Load SecretKey **/
KeyStore ks = KeyStore.getInstance(ANDROID_KEYSTORE);
ks.load(null);
if (!ks.containsAlias(MASTER_KEY_ALIAS)) {
throw new KeyException("Key alias not found: "+ MASTER_KEY_ALIAS);
}
SecretKey secretKey = (SecretKey) ks.getKey(MASTER_KEY_ALIAS, null);
/** Read encrypted file **/
File encryptedFile = new File(context.getFilesDir(), fileName);
InputStream inputStream = Files.newInputStream(encryptedFile.toPath(), READ);
byte[] iv = new byte[IV_SIZE_IN_BYTES];
int ciphertextSize = inputStream.available() - IV_SIZE_IN_BYTES;
byte[] ciphertext = new byte[ciphertextSize];
inputStream.read(iv, 0, IV_SIZE_IN_BYTES);
inputStream.read(ciphertext, 0, ciphertextSize);
inputStream.close();
/** Decrypt data **/
Cipher cipher = Cipher.getInstance(AES_GCM_NOPADDING);
GCMParameterSpec spec = new GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
cipher.updateAAD(encryptedFile.getName().getBytes(UTF_8));
byte[] plaintext = cipher.doFinal(ciphertext);
/** Print decrypted message **/
System.out.println(
new String(plaintext, UTF_8)
);
} catch(Exception e){
e.printStackTrace();
}
}
@SuppressLint({"BlockedPrivateApi"})
private static void initAndroidKeystore() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
/** static AndroidKeyStoreProvider.install() **/
Class cAndroidKeyStoreProvider = Class.forName("android.security.keystore2.AndroidKeyStoreProvider");
Method install = cAndroidKeyStoreProvider.getDeclaredMethod("install");
install.invoke(cAndroidKeyStoreProvider);
}
private static Context initAppContext(String packageName) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
/** Required for ActivityThread **/
Looper.prepareMainLooper();
/** static ActivityThread.systemMain() **/
Class cActivityThread = Class.forName("android.app.ActivityThread");
Method systemMain = cActivityThread.getDeclaredMethod("systemMain");
Object activityThread = systemMain.invoke(cActivityThread);
/** virtual activityThread.getPackageInfo(...) **/
Class cCompatibilityInfo = Class.forName("android.content.res.CompatibilityInfo");
Constructor compatibilityInfoConstructor = cCompatibilityInfo.getConstructor(ApplicationInfo.class, int.class, int.class, boolean.class);
Object compatibilityInfo = compatibilityInfoConstructor.newInstance(new ApplicationInfo(), 0x02, 200, true);
Method getPackageInfo = cActivityThread.getDeclaredMethod("getPackageInfo", String.class, cCompatibilityInfo, int.class);
Object loadedApk = getPackageInfo.invoke(activityThread, packageName, compatibilityInfo, 0);
/** virtual loadedApk.makeApplication(...) **/
Class cLoadedApk = Class.forName("android.app.LoadedApk");
Method makeApplication = cLoadedApk.getDeclaredMethod("makeApplication", boolean.class, Instrumentation.class);
Application application = (Application) makeApplication.invoke(loadedApk,true, null);
return application.getApplicationContext();
}
}
Uwierzytelnianie blokadą ekranu
Aby wymusić i obsłużyć uwierzytelnianie klucza blokadą ekranu, ustawiamy:
- podczas generowania klucza (KeyGenParameterSpec.Builder) setUserAuthenticationRequired(true) oraz setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
[https://developer.android.com/reference/android/security/keystore/KeyProperties#AUTH_DEVICE_CREDENTIAL] - podczas uwierzytelniania klucza (BiometricPrompt.PromptInfo.Builder) setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
[https://developer.android.com/reference/androidx/biometric/BiometricManager.Authenticators#DEVICE_CREDENTIAL()]
Bazowa aplikacja
Implementując funkcję initialize(), bazujemy na kodzie z testów dla StrongBox, zmieniamy alias, wymuszamy uwierzytelnianie oraz przekazujemy odpowiednio zbudowany PromptInfo.
Nazwa pakietu: pl.isec.baseapp.devicecredential
MainActivity.java
private void initialize() throws GeneralSecurityException, IOException {
/** Get or create key **/
KeyGenParameterSpec masterKeySpec = createAES256GCMKeyGenParameterSpec();
String masterKeyAlias = masterKeySpec.getKeystoreAlias();
KeyStore ks = KeyStore.getInstance(ANDROID_KEYSTORE);
ks.load(null);
if (!ks.containsAlias(masterKeyAlias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
keyGenerator.init(masterKeySpec);
keyGenerator.generateKey();
}
/** Get security level **/
SecretKey secretKey = (SecretKey) ks.getKey(masterKeyAlias, null);
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), ANDROID_KEYSTORE);
KeyInfo keyInfo = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class);
switch(keyInfo.getSecurityLevel()){
case KeyProperties.SECURITY_LEVEL_SOFTWARE:
mSecurityLevel = "Software"; break;
case KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT:
mSecurityLevel = "TEE"; break;
case KeyProperties.SECURITY_LEVEL_STRONGBOX:
mSecurityLevel = "StrongBox"; break;
default:
mSecurityLevel = "Unknown";
}
/** Build PromptInfo for SecureFile **/
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock encryption key")
.setSubtitle("Log in using your device credential")
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build();
/** Create instance of the SecureFile **/
mSecureFile = new SecureFileBiometricImpl(
new File(getFilesDir(), FILENAME),
this,
masterKeyAlias,
MainActivity.this,
promptInfo
);
}
private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(){
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
"file_encryption_master_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
.setInvalidatedByBiometricEnrollment(true);
return builder.build();
}
Instalujemy aplikację:
$ adb install ~/AndroidStudioProjects/KsDeviceCredential/app/release/app-release.apk
Uruchamiamy:
I otrzymujemy komunikat, że coś poszło nie tak…
W logach urządzenia (komenda adb logcat) znajdujemy jednoznaczną informację o źródle problemu.
System.err: java.security.InvalidAlgorithmParameterException: java.lang.IllegalStateException: Secure lock screen must be enabled to create keys requiring user authentication
System.err: at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineInit(AndroidKeyStoreKeyGeneratorSpi.java:262)
System.err: at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi$AES.engineInit(AndroidKeyStoreKeyGeneratorSpi.java:63)
System.err: at javax.crypto.KeyGenerator.init(KeyGenerator.java:519)
System.err: at javax.crypto.KeyGenerator.init(KeyGenerator.java:502)
System.err: at pl.isec.baseapp.devicecredential.MainActivity.initialize(MainActivity.java:126)
System.err: at pl.isec.baseapp.devicecredential.MainActivity.onCreate(MainActivity.java:50)
System.err: at android.app.Activity.performCreate(Activity.java:8290)
System.err: at android.app.Activity.performCreate(Activity.java:8269)
System.err: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1384)
System.err: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3657)
System.err: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3813)
System.err: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
System.err: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
System.err: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
System.err: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308)
System.err: at android.os.Handler.dispatchMessage(Handler.java:106)
System.err: at android.os.Looper.loopOnce(Looper.java:201)
System.err: at android.os.Looper.loop(Looper.java:288)
System.err: at android.app.ActivityThread.main(ActivityThread.java:7898)
System.err: at java.lang.reflect.Method.invoke(Native Method)
System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
System.err: Caused by: java.lang.IllegalStateException: Secure lock screen must be enabled to create keys requiring user authentication
System.err: at android.security.keystore2.KeyStore2ParameterUtils.getRootSid(KeyStore2ParameterUtils.java:234)
System.err: at android.security.keystore2.KeyStore2ParameterUtils.addSids(KeyStore2ParameterUtils.java:286)
System.err: at android.security.keystore2.KeyStore2ParameterUtils.addUserAuthArgs(KeyStore2ParameterUtils.java:329)
System.err: at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineInit(AndroidKeyStoreKeyGeneratorSpi.java:260)
System.err: ... 21 more
Otóż, na urządzeniu musi być skonfigurowana blokada ekranu, aby móc ją wykorzystać do uwierzytelniania klucza :)
Konfigurujemy blokadę, uruchamiamy ponownie aplikację i zapisujemy tajną wiadomość.
Urządzenie poprosi nas o odblokowanie klucza zarówno przy próbie zapisu jak i odczytu wiadomości.
Aplikacja intruza
Wysyłamy na urządzenie przygotowaną wcześniej konsolową aplikację intruza:
$ adb push ~/AndroidStudioProjects/KsRobberConsoleSecureFileBiometricImpl/app/release/app-release.apk /data/local/tmp/robber_v2.apk
Uruchamiamy ją z odpowiednimi uprawnieniami, w sposób znany z pierwszej części artykułu:
$ adb shell
sunfish:/ $ su
sunfish:/ # pm list packages -U pl.isec.baseapp.devicecredential
package:pl.isec.baseapp.devicecredential uid:10231
sunfish:/ # su 10231
sunfish:/ $ app_process -cp /data/local/tmp/robber_v2.apk /system/bin pl.isec.robber.console.securefilebiometricimpl.Run pl.isec.baseapp.devicecredential encrypted.txt
javax.crypto.IllegalBlockSizeException
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:590)
at javax.crypto.Cipher.doFinal(Cipher.java:2056)
at pl.isec.robber.console.securefilebiometricimpl.Run.main(Run.java:74)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:355)
Caused by: android.security.KeyStoreException: Key user not authenticated (internal Keystore code: -26 message: In KeystoreOperation::updateAad
Caused by:
0: In update_aad: Trying to get auth tokens.
1: In AuthInfo::get_auth_tokens.
2: In get_auth_tokens: No operation auth token received.
3: Error::Km(ErrorCode(-26))) (public error code: 2 internal Keystore code: -26)
at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:369)
at android.security.KeyStoreOperation.handleExceptions(KeyStoreOperation.java:78)
at android.security.KeyStoreOperation.updateAad(KeyStoreOperation.java:100)
at android.security.keystore2.AndroidKeyStoreAuthenticatedAESCipherSpi$AdditionalAuthenticationDataStream.update(AndroidKeyStoreAuthenticatedAESCipherSpi.java:435)
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.update(KeyStoreCryptoOperationChunkedStreamer.java:156)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineUpdateAAD(AndroidKeyStoreCipherSpiBase.java:535)
at javax.crypto.Cipher.updateAAD(Cipher.java:2616)
at javax.crypto.Cipher.updateAAD(Cipher.java:2570)
at pl.isec.robber.console.securefilebiometricimpl.Run.main(Run.java:73)
... 2 more
sunfish:/ $
Jest sukces! :)
Udało nam się zablokować dostęp do poufnych danych przed intruzem, który posiada w systemie bardzo wysokie uprawnienia.
Uwierzytelnianie biometrią
Konfiguracja keystore z biometrią niewiele różni się od konfiguracji z blokadą ekranu, zmieniamy jedynie typ uwierzytelniania:
- podczas generowania klucza (KeyGenParameterSpec.Builder) setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
[https://developer.android.com/reference/android/security/keystore/KeyProperties#AUTH_BIOMETRIC_STRONG] - podczas uwierzytelniania klucza (BiometricPrompt.PromptInfo.Builder) setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
[https://developer.android.com/reference/androidx/biometric/BiometricManager.Authenticators#BIOMETRIC_STRONG()]
oraz ustawiamy wymagany dla biometrii (BiometricPrompt.PromptInfo.Builder) setNegativeButtonText(CharSequence negativeButtonText).
Bazowa aplikacja
Nazwa pakietu: pl.isec.baseapp.biometricstrong
MainActivity.java
private void initialize() throws GeneralSecurityException, IOException {
/** Get or create key **/
KeyGenParameterSpec masterKeySpec = createAES256GCMKeyGenParameterSpec();
String masterKeyAlias = masterKeySpec.getKeystoreAlias();
KeyStore ks = KeyStore.getInstance(ANDROID_KEYSTORE);
ks.load(null);
if (!ks.containsAlias(masterKeyAlias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
keyGenerator.init(masterKeySpec);
keyGenerator.generateKey();
}
/** Get security level **/
SecretKey secretKey = (SecretKey) ks.getKey(masterKeyAlias, null);
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), ANDROID_KEYSTORE);
KeyInfo keyInfo = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class);
switch(keyInfo.getSecurityLevel()){
case KeyProperties.SECURITY_LEVEL_SOFTWARE:
mSecurityLevel = "Software"; break;
case KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT:
mSecurityLevel = "TEE"; break;
case KeyProperties.SECURITY_LEVEL_STRONGBOX:
mSecurityLevel = "StrongBox"; break;
default:
mSecurityLevel = "Unknown";
}
/** Build PromptInfo for SecureFile **/
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock encryption key")
.setSubtitle("Log in using your biometric")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build();
/** Create instance of the SecureFile **/
mSecureFile = new SecureFileBiometricImpl(
new File(getFilesDir(), FILENAME),
this,
masterKeyAlias,
MainActivity.this,
promptInfo
);
}
private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(){
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
"file_encryption_master_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.setInvalidatedByBiometricEnrollment(true);
return builder.build();
}
Instalujemy i uruchamiamy aplikację.
$ adb install ~/AndroidStudioProjects/KsBiometricStrong/app/release/app-release.apk
Podobnie jak poprzednio, ustawienie odpowiedniego typu uwierzytelniania, pociąga za sobą konsekwencje w konfiguracji urządzenia:
- uwierzytelnianie blokadą ekranu wymusza na użytkownikowi skonfigurowanie blokady ekranu
- uwierzytelnianie biometrią wymusza na użytkownikowi skonfigurowanie biometrii
System.err: java.security.InvalidAlgorithmParameterException: java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use
System.err: at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineInit(AndroidKeyStoreKeyGeneratorSpi.java:262)
System.err: at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi$AES.engineInit(AndroidKeyStoreKeyGeneratorSpi.java:63)
System.err: at javax.crypto.KeyGenerator.init(KeyGenerator.java:519)
System.err: at javax.crypto.KeyGenerator.init(KeyGenerator.java:502)
System.err: at pl.isec.baseapp.biometricstrong.MainActivity.initialize(MainActivity.java:123)
System.err: at pl.isec.baseapp.biometricstrong.MainActivity.onCreate(MainActivity.java:47)
System.err: at android.app.Activity.performCreate(Activity.java:8290)
System.err: at android.app.Activity.performCreate(Activity.java:8269)
System.err: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1384)
System.err: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3657)
System.err: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3813)
System.err: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
System.err: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
System.err: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
System.err: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2308)
System.err: at android.os.Handler.dispatchMessage(Handler.java:106)
System.err: at android.os.Looper.loopOnce(Looper.java:201)
System.err: at android.os.Looper.loop(Looper.java:288)
System.err: at android.app.ActivityThread.main(ActivityThread.java:7898)
System.err: at java.lang.reflect.Method.invoke(Native Method)
System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
System.err: Caused by: java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use
System.err: at android.security.keystore2.KeyStore2ParameterUtils.addSids(KeyStore2ParameterUtils.java:266)
System.err: at android.security.keystore2.KeyStore2ParameterUtils.addUserAuthArgs(KeyStore2ParameterUtils.java:329)
System.err: at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineInit(AndroidKeyStoreKeyGeneratorSpi.java:260)
System.err: ... 21 more
Konfigurujemy biometrię i zapisujemy wiadomość.
Aplikacja intruza
Dla formalności, spróbujmy odczytać wiadomość aplikacją intruza:
$ adb shell
sunfish:/ $ su
sunfish:/ # pm list packages -U pl.isec.baseapp.biometricstrong
package:pl.isec.baseapp.biometricstrong uid:10240
sunfish:/ # su 10240
sunfish:/ $ app_process -cp /data/local/tmp/robber_v2.apk /system/bin pl.isec.robber.console.securefilebiometricimpl.Run pl.isec.baseapp.biometricstrong encrypted.txt
javax.crypto.IllegalBlockSizeException
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:590)
at javax.crypto.Cipher.doFinal(Cipher.java:2056)
at pl.isec.robber.console.securefilebiometricimpl.Run.main(Run.java:74)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:355)
Caused by: android.security.KeyStoreException: Key user not authenticated (internal Keystore code: -26 message: In KeystoreOperation::updateAad
Caused by:
0: In update_aad: Trying to get auth tokens.
1: In AuthInfo::get_auth_tokens.
2: In get_auth_tokens: No operation auth token received.
3: Error::Km(ErrorCode(-26))) (public error code: 2 internal Keystore code: -26)
at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:369)
at android.security.KeyStoreOperation.handleExceptions(KeyStoreOperation.java:78)
at android.security.KeyStoreOperation.updateAad(KeyStoreOperation.java:100)
at android.security.keystore2.AndroidKeyStoreAuthenticatedAESCipherSpi$AdditionalAuthenticationDataStream.update(AndroidKeyStoreAuthenticatedAESCipherSpi.java:435)
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.update(KeyStoreCryptoOperationChunkedStreamer.java:156)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineUpdateAAD(AndroidKeyStoreCipherSpiBase.java:535)
at javax.crypto.Cipher.updateAAD(Cipher.java:2616)
at javax.crypto.Cipher.updateAAD(Cipher.java:2570)
at pl.isec.robber.console.securefilebiometricimpl.Run.main(Run.java:73)
... 2 more
sunfish:/ $
Jak można było się spodziewać, uwierzytelnianie biometrią sprawdza się równie dobrze i zapewnia skuteczną ochronę przed nieuprawnionym użyciem kluczy przechowywanych w Android Keystore.
Blokada ekranu czy biometria?
Różne sposoby uwierzytelniania mają swoje słabości, dla przykładu, PIN i hasło mogą zostać przekazane osobie trzeciej, a odblokowanie poprzez Face ID może nastąpić całkiem przypadkowo i nieświadomie.
Natomiast, z punktu widzenia dewelopera korzystającego z usługi uwierzytelniania kluczy w Android Keystore, nie ma to znaczenia, liczy się fakt, że klucz został uwierzytelniony.
O ile wymuszenie skonfigurowania blokady ekranu (PIN/hasło/wzór) przez aplikację operującą na wrażliwych danych (np. aplikacja bankowa) może być zrozumiałe, o tyle wymuszanie skonfigurowania biometrii może nie zostać pozytywnie odebrane.
Warto dać użytkownikowi wybór konfigurując aplikację do obsługi uwierzytelniania zarówno klasyczną blokadą ekranu jak i biometrią.
Przykład rozwiązania znajdziemy w oficjalnym tutorialu: https://developer.android.com/training/sign-in/biometric-auth#biometric-or-lock-screen
Deweloperzy aplikacji powinni zainteresować się również możliwością weryfikacji, czy urządzenie posiada skonfigurowaną blokadę ekranu bądź biometrię, zanim przystąpią do próby wygenerowania klucza:
- https://developer.android.com/reference/android/app/KeyguardManager#isDeviceSecure()
- https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
Czasowe uwierzytelnianie klucza
Wspomniana już funkcja setUserAuthenticationParameters(int timeout, int type) pozwala ustawić nie tylko typ uwierzytelniania, ale także czas na jaki klucz zostanie odblokowany.
Zgodnie z dokumentacją, nasze dotychczasowe ustawienia, wymuszały odblokowanie klucza przy każdym użyciu.
timeout int: duration in seconds or 0 if user authentication must take place for every use of the key. Value is 0 or greater
Zatem, sprawdźmy czy czasowe uwierzytelnianie da intruzowi sposobność do dyskretnego użycia klucza w odblokowanym przedziale czasowym.
Bazowa aplikacja będzie niewielką modyfikacją wersji z poprzedniego testu:
- ustawimy czas uwierzytelnienia na 30 sekund
- pozwolimy na uwierzytelnianie zarówno biometrią jak i blokadą ekranu
Mała uwaga: Przy uwierzytelnianiu biometrią i blokadą ekranu wraz z czasowym uwierzytelnianiem, należy zrezygnować z funkcji setInvalidatedByBiometricEnrollment(boolean).
Bazowa aplikacja
Nazwa pakietu: pl.isec.baseapp.authenticationfor30s
MainActivity.java
private void initialize() throws GeneralSecurityException, IOException {
/** Get or create key **/
KeyGenParameterSpec masterKeySpec = createAES256GCMKeyGenParameterSpec();
String masterKeyAlias = masterKeySpec.getKeystoreAlias();
KeyStore ks = KeyStore.getInstance(ANDROID_KEYSTORE);
ks.load(null);
if (!ks.containsAlias(masterKeyAlias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
keyGenerator.init(masterKeySpec);
keyGenerator.generateKey();
}
/** Get security level **/
SecretKey secretKey = (SecretKey) ks.getKey(masterKeyAlias, null);
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), ANDROID_KEYSTORE);
KeyInfo keyInfo = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class);
switch(keyInfo.getSecurityLevel()){
case KeyProperties.SECURITY_LEVEL_SOFTWARE:
mSecurityLevel = "Software"; break;
case KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT:
mSecurityLevel = "TEE"; break;
case KeyProperties.SECURITY_LEVEL_STRONGBOX:
mSecurityLevel = "StrongBox"; break;
default:
mSecurityLevel = "Unknown";
}
/** Build PromptInfo for SecureFile **/
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock encryption key")
.setSubtitle("Log in using your biometric or device credential")
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL | BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build();
/** Create instance of the SecureFile **/
mSecureFile = new SecureFileBiometricImpl(
new File(getFilesDir(), FILENAME),
this,
masterKeyAlias,
MainActivity.this,
promptInfo
);
}
private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(){
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
"file_encryption_master_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(30, KeyProperties.AUTH_DEVICE_CREDENTIAL | KeyProperties.AUTH_BIOMETRIC_STRONG);
return builder.build();
}
Instalujemy, uruchamiamy aplikację i zapisujemy kolejną wiadomość.
$ adb install ~/AndroidStudioProjects/KsAuthenticationFor30s/app/release/app-release.apk
Aplikacja intruza
Uruchamiamy test w przeciągu 30 sekund od pomyślnego uwierzytelnienia:
$ adb shell
sunfish:/ $ su
sunfish:/ # pm list packages -U pl.isec.baseapp.authenticationfor30s
package:pl.isec.baseapp.authenticationfor30s uid:10254
sunfish:/ # su 10254
sunfish:/ $ app_process -cp /data/local/tmp/robber_v2.apk /system/bin pl.isec.robber.console.securefilebiometricimpl.Run pl.isec.baseapp.authenticationfor30s encrypted.txt
Okno czasowe bez zabezpieczeń antywłamaniowych :)
sunfish:/ $
Po przekroczeniu 30 sekund:
sunfish:/ $ app_process -cp /data/local/tmp/robber_v2.apk /system/bin pl.isec.robber.console.securefilebiometricimpl.Run pl.isec.baseapp.authenticationfor30s encrypted.txt <
android.security.keystore.UserNotAuthenticatedException: User not authenticated
at android.security.keystore2.KeyStoreCryptoOperationUtils.getInvalidKeyException(KeyStoreCryptoOperationUtils.java:128)
at android.security.keystore2.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:154)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:339)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:234)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2981)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2892)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2797)
at javax.crypto.Cipher.chooseProvider(Cipher.java:774)
at javax.crypto.Cipher.init(Cipher.java:1289)
at javax.crypto.Cipher.init(Cipher.java:1224)
at pl.isec.robber.console.securefilebiometricimpl.Run.main(Run.java:72)
at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:355)
Wykorzystując krótki okres czasu, udało nam się odczytać wiadomość za pomocą aplikacji nieprzystosowanej do uwierzytelniania kluczy!
Zadziałało to, gdyż uwierzytelnianie w Android Keystore zaimplementowane jest na niskim poziomie, w systemowej usłudze.
Z tego powodu, żadne modyfikacje naszej aplikacji nie pozwolą obejść mechanizmu uwierzytelniania.
Z drugiej strony, raz odblokowany klucz z niezerowym czasem podtrzymywania autoryzacji, pozostaje dostępny na określony czas, jak gdyby nie był chroniony uwierzytelnianiem.
Drobny błąd i jego fatalne konsekwencje
Korzystając z aplikacji, dla której zaimplementowaliśmy 30 sekundowe okno czasowe, możemy odnieść wrażenie, że aplikacja i tak wymaga uwierzytelnienia przy każdym użyciu klucza.
Czy coś poszło nie tak?
Mając na uwadze powyższe obserwacje oraz wyniki testów z aplikacją intruza, dochodzimy do prostego wniosku, że ta potrzeba każdorazowego uwierzytelnienia, wynika jedynie z mało profesjonalnej implementacji interfejsu aplikacji, która nie sprawdza, czy wykorzystanie klucza wymaga podjęcia dodatkowych akcji.
Ponadto, dowiedzieliśmy się, że Android Keystore nie zwróci żadnego błędu przy uwierzytelnianiu klucza, który tego uwierzytelniania nie wymaga.
Tym sposobem dochodzimy do ciekawego przypadku, w którym aplikacja będzie wymagała i obsługiwała uwierzytelnianie przy każdym użyciu klucza, jednak w implementacji znajdzie się drobny błąd przy jego generowaniu, polegający na pominięciu funkcji setUserAuthenticationRequired(true).
Bazowa aplikacja
Nazwa pakietu: pl.isec.baseapp.invalidauthentication
MainActivity.java
private void initialize() throws GeneralSecurityException, IOException {
/** Get or create key **/
KeyGenParameterSpec masterKeySpec = createAES256GCMKeyGenParameterSpec();
String masterKeyAlias = masterKeySpec.getKeystoreAlias();
KeyStore ks = KeyStore.getInstance(ANDROID_KEYSTORE);
ks.load(null);
if (!ks.containsAlias(masterKeyAlias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
keyGenerator.init(masterKeySpec);
keyGenerator.generateKey();
}
/** Get security level **/
SecretKey secretKey = (SecretKey) ks.getKey(masterKeyAlias, null);
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), ANDROID_KEYSTORE);
KeyInfo keyInfo = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class);
switch(keyInfo.getSecurityLevel()){
case KeyProperties.SECURITY_LEVEL_SOFTWARE:
mSecurityLevel = "Software"; break;
case KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT:
mSecurityLevel = "TEE"; break;
case KeyProperties.SECURITY_LEVEL_STRONGBOX:
mSecurityLevel = "StrongBox"; break;
default:
mSecurityLevel = "Unknown";
}
/** Build PromptInfo for SecureFile **/
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock encryption key")
.setSubtitle("Log in using your biometric or device credential")
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL | BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build();
/** Create instance of the SecureFile **/
mSecureFile = new SecureFileBiometricImpl(
new File(getFilesDir(), FILENAME),
this,
masterKeyAlias,
MainActivity.this,
promptInfo
);
}
private static KeyGenParameterSpec createAES256GCMKeyGenParameterSpec(){
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
"file_encryption_master_key",
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true)
//.setUserAuthenticationRequired(true) // oh no :)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL | KeyProperties.AUTH_BIOMETRIC_STRONG);
return builder.build();
}
Instalujemy aplikację, uruchamiamy i zapisujemy wiadomość.
$ adb install ~/AndroidStudioProjects/KsInvalidAuthentication/app/release/app-release.apk
Interfejs aplikacji wymaga uwierzytelniania zarówno przy zapisie jak i odczycie wiadomości.
Anulowanie uwierzytelniania kończy się stosownym błędem.
Aplikacja intruza
Sprawdźmy zatem, czy drobny błąd przy generowaniu klucza może zburzyć wysiłek włożony w obsługę uwierzytelniania.
$ adb shell
sunfish:/ $ su
sunfish:/ # pm list packages -U pl.isec.baseapp.invalidauthentication
package:pl.isec.baseapp.invalidauthentication uid:10256
sunfish:/ # su 10256
sunfish:/ $ app_process -cp /data/local/tmp/robber_v2.apk /system/bin pl.isec.robber.console.securefilebiometricimpl.Run pl.isec.baseapp.invalidauthentication encrypted.txt
Czy to jest bezpieczne? :)
sunfish:/ $
A jednak! Wszystko co zaimplementowane jest w zwykłej aplikacji da się w pewien sposób obejść, a dla usługi Android Keystore liczą się jedynie twardo zdefiniowane wymagania :)
Wnioski
Potwierdziliśmy, że wymaganie uwierzytelnienia klucza przechowywanego w Android Keystore, potrafi zabezpieczyć poufne dane przed intruzem posiadającym bardzo wysokie uprawnienia w systemie. Swego rodzaju “minusem” tego rozwiązania, jest wymuszenie na użytkowniku skonfigurowania odpowiednich zabezpieczeń urządzenia.
Dowiedliśmy, że udogodnienie dla deweloperów i użytkowników, jakimi są klucze uwierzytelniane na zadany czas, wprowadza pewne słabości, w postaci krótkich okien czasowych, w których potencjalny intruz może skorzystać z materiału szyfrującego.
Ostatni przykład pokazał nam z jak wielką uwagą należy definiować wymagania podczas procesu generowania klucza, gdyż prosty błąd może sprawić, że aplikacja obsługująca uwierzytelnianie biometrią i blokadą ekranu, w rzeczywistości bezpieczeństwo danych będzie opierać na kluczach niechronionych uwierzytelnianiem.