Biometric encryption

This commit is contained in:
MrLetsplay 2023-09-18 20:27:56 +02:00
parent 8fbf44b75a
commit ffba642fa0
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
12 changed files with 266 additions and 65 deletions

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="R38N50464FV" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-09-18T17:20:49.048823275Z" />
</component>
</project>

View File

@ -10,8 +10,8 @@
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="/mnt/sshd/Files/Desktop/testing/android/Cringe-Authenticator" />
<option value="/mnt/sshd/Files/Desktop/testing/android/Cringe-Authenticator/app" />
</set>
</option>
</GradleProjectSettings>

View File

@ -74,7 +74,6 @@ dependencies {
implementation 'com.google.mlkit:barcode-scanning:17.1.0'
implementation 'com.google.code.gson:gson:2.8.9'
def camerax_version = "1.2.3"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

View File

@ -0,0 +1,26 @@
package com.cringe_studios.cringe_authenticator.crypto;
public class BiometricKey {
private final String id;
private final byte[] encryptedKey;
private final byte[] iv;
public BiometricKey(String id, byte[] encryptedKey, byte[] iv) {
this.id = id;
this.encryptedKey = encryptedKey;
this.iv = iv;
}
public String getId() {
return id;
}
public byte[] getEncryptedKey() {
return encryptedKey;
}
public byte[] getIV() {
return iv;
}
}

View File

@ -1,17 +1,37 @@
package com.cringe_studios.cringe_authenticator.crypto;
import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
@ -19,6 +39,8 @@ import javax.crypto.spec.SecretKeySpec;
public class Crypto {
private static final String KEY_STORE = "AndroidKeyStore";
public static byte[] generateHash(CryptoParameters parameters, String password) throws CryptoException {
Argon2Parameters params = new Argon2Parameters.Builder()
.withVersion(parameters.getArgon2Version())
@ -55,12 +77,16 @@ public class Crypto {
return generateBytes(parameters.getEncryptionIVLength());
}
public static byte[] encrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
public static CryptoResult encryptWithResult(CryptoParameters parameters, byte[] bytes, SecretKey key, boolean useIV) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(parameters.getEncryptionAlgorithm());
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, parameters.getIV());
if(useIV) {
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
return cipher.doFinal(bytes);
}else {
cipher.init(Cipher.ENCRYPT_MODE, key);
}
return new CryptoResult(cipher.doFinal(bytes), cipher.getIV());
}catch(NoSuchAlgorithmException | NoSuchPaddingException |
InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException |
IllegalBlockSizeException e) {
@ -68,10 +94,14 @@ public class Crypto {
}
}
public static byte[] decrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
public static byte[] encrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
return encryptWithResult(parameters, bytes, key, true).getEncrypted();
}
public static byte[] decrypt(CryptoParameters parameters, byte[] bytes, SecretKey key, byte[] overrideIV) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(parameters.getEncryptionAlgorithm());
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, parameters.getIV());
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, overrideIV != null ? overrideIV : parameters.getIV());
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(bytes);
}catch(NoSuchAlgorithmException | NoSuchPaddingException |
@ -81,4 +111,53 @@ public class Crypto {
}
}
public static byte[] decrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
return decrypt(parameters, bytes, key, null);
}
@RequiresApi(api = Build.VERSION_CODES.M)
public static BiometricKey createBiometricKey(CryptoParameters parameters) throws CryptoException {
try {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE);
String keyID = UUID.randomUUID().toString();
generator.init(new KeyGenParameterSpec.Builder(keyID,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setKeySize(parameters.getEncryptionAESKeyLength() * 8)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(60)
.setRandomizedEncryptionRequired(true)
.build());
SecretKey biometricKey = generator.generateKey();
SecretKey key = OTPDatabase.getLoadedKey();
CryptoResult encryptedKey = Crypto.encryptWithResult(parameters, key.getEncoded(), biometricKey, false);
return new BiometricKey(keyID, encryptedKey.getEncrypted(), encryptedKey.getIV());
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new CryptoException(e);
}
}
public static SecretKey getBiometricKey(BiometricKey key) throws CryptoException {
try {
KeyStore store = KeyStore.getInstance(KEY_STORE);
store.load(null);
return (SecretKey) store.getKey(key.getId(), null);
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException | IOException e) {
throw new CryptoException(e);
}
}
public static void deleteBiometricKey(BiometricKey key) throws CryptoException {
try {
KeyStore ks = KeyStore.getInstance(KEY_STORE);
ks.load(null);
ks.deleteEntry(key.getId());
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new CryptoException(e);
}
}
}

View File

@ -0,0 +1,20 @@
package com.cringe_studios.cringe_authenticator.crypto;
public class CryptoResult {
private byte[] encrypted;
private byte[] iv;
public CryptoResult(byte[] encrypted, byte[] iv) {
this.encrypted = encrypted;
this.iv = iv;
}
public byte[] getEncrypted() {
return encrypted;
}
public byte[] getIV() {
return iv;
}
}

View File

@ -3,6 +3,7 @@ package com.cringe_studios.cringe_authenticator.fragment;
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG;
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@ -16,10 +17,12 @@ import androidx.annotation.Nullable;
import androidx.biometric.BiometricManager;
import com.cringe_studios.cringe_authenticator.MainActivity;
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
import com.cringe_studios.cringe_authenticator.databinding.FragmentSettingsBinding;
import com.cringe_studios.cringe_authenticator.util.BiometricUtil;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.FabUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
@ -28,6 +31,7 @@ import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import javax.crypto.SecretKey;
@ -106,9 +110,35 @@ public class SettingsFragment extends NamedFragment {
binding.settingsEnableIntroVideo.setChecked(SettingsUtil.isIntroVideoEnabled(requireContext()));
binding.settingsEnableIntroVideo.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setEnableIntroVideo(requireContext(), checked));
if(BiometricManager.from(requireContext()).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) {
binding.settingsBiometricLock.setChecked(SettingsUtil.isBiometricLock(requireContext()));
binding.settingsBiometricLock.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setBiometricLock(requireContext(), checked));
if(SettingsUtil.isDatabaseEncrypted(requireContext())
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& BiometricManager.from(requireContext()).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) {
binding.settingsBiometricLock.setChecked(SettingsUtil.isBiometricEncryption(requireContext()));
binding.settingsBiometricLock.setOnCheckedChangeListener((view, checked) -> {
if(checked) {
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
Log.i("PROMPT", "§BERIIO");
BiometricUtil.promptBiometricAuth(requireActivity(), () -> {
try {
BiometricKey biometricKey = Crypto.createBiometricKey(SettingsUtil.getCryptoParameters(requireContext()));
SettingsUtil.enableBiometricEncryption(requireContext(), biometricKey);
} catch (CryptoException e) {
e.printStackTrace();
DialogUtil.showErrorDialog(requireContext(), "Failed to enable: " + e);
}
}, () -> view.setChecked(false));
}, null);
}else {
try {
BiometricKey key = SettingsUtil.getBiometricKey(requireContext());
if(key != null) Crypto.deleteBiometricKey(key);
} catch (CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), "Failed to delete key: " + e);
}
SettingsUtil.disableBiometricEncryption(requireContext());
}
});
}else {
binding.settingsBiometricLock.setChecked(false);
binding.settingsBiometricLock.setEnabled(false);

View File

@ -18,9 +18,11 @@ import androidx.core.content.ContextCompat;
import com.cringe_studios.cringe_authenticator.MainActivity;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.databinding.ActivityUnlockBinding;
import com.cringe_studios.cringe_authenticator.util.BiometricUtil;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
@ -36,6 +38,7 @@ import java.util.concurrent.Executor;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class UnlockActivity extends AppCompatActivity {
@ -57,47 +60,20 @@ public class UnlockActivity extends AppCompatActivity {
binding = ActivityUnlockBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if(SettingsUtil.isBiometricEncryption(this) && BiometricUtil.isSupported(this)) {
binding.unlockBiometrics.setOnClickListener(view -> BiometricUtil.promptBiometricAuth(this, this::success, () -> {}));
BiometricUtil.promptBiometricAuth(this, () -> {
BiometricKey biometricKey = SettingsUtil.getBiometricKey(this);
try {
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
SecretKey key = KeyGenerator.getInstance("AES").generateKey();
ks.setEntry("amogus", new KeyStore.SecretKeyEntry(key), new KeyStoreParameter.Builder(this).build());
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
if(SettingsUtil.isBiometricLock(this)) {
Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt prompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
failure();
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
success();
}
});
boolean supportsBiometricAuth = BiometricManager.from(this).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
boolean recentlyUnlocked = savedInstanceState != null && (System.currentTimeMillis() - savedInstanceState.getLong("pauseTime", 0L) < LOCK_TIMEOUT);
if(!recentlyUnlocked && SettingsUtil.isBiometricLock(this) && supportsBiometricAuth) {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.app_name))
.setSubtitle(getString(R.string.biometric_lock_subtitle))
.setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
.build();
//prompt.authenticate(info);
binding.unlockBiometrics.setOnClickListener(view -> {
//prompt.authenticate(info);
});
}else {
SecretKey biometricSecretKey = Crypto.getBiometricKey(biometricKey);
byte[] keyBytes = Crypto.decrypt(SettingsUtil.getCryptoParameters(this), biometricKey.getEncryptedKey(), biometricSecretKey, biometricKey.getIV());
SecretKey key = new SecretKeySpec(keyBytes, "AES");
OTPDatabase.loadDatabase(this, key);
success();
} catch (CryptoException | OTPDatabaseException e) {
DialogUtil.showErrorDialog(this, "Failed to load database: " + e);
}
}, () -> {});
}
binding.unlockButton.setOnClickListener(view -> {

View File

@ -0,0 +1,52 @@
package com.cringe_studios.cringe_authenticator.util;
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG;
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import com.cringe_studios.cringe_authenticator.R;
import java.util.concurrent.Executor;
public class BiometricUtil {
public static boolean isSupported(Context context) {
return BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
}
public static void promptBiometricAuth(FragmentActivity context, Runnable success, Runnable failure) {
if(!isSupported(context)) {
failure.run();
return;
}
Executor executor = ContextCompat.getMainExecutor(context);
BiometricPrompt prompt = new BiometricPrompt(context, executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
failure.run();
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
success.run();
}
});
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.app_name))
.setSubtitle(context.getString(R.string.biometric_lock_subtitle))
.setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
.build();
prompt.authenticate(info);
}
}

View File

@ -182,6 +182,10 @@ public class OTPDatabase {
return loadedDatabase;
}
public static SecretKey getLoadedKey() {
return loadedKey;
}
public static void encrypt(Context ctx, SecretKey key, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
loadedKey = key;

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences;
import android.util.Base64;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.crypto.BiometricKey;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
import com.google.gson.Gson;
@ -120,6 +121,8 @@ public class SettingsUtil {
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit()
.putBoolean("encryption", false)
.remove("encryption.parameters")
.remove("encryption.biometric")
.remove("encryption.biometric.key")
.apply();
}
@ -127,10 +130,10 @@ public class SettingsUtil {
return GSON.fromJson(ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("encryption.parameters", "{}"), CryptoParameters.class);
}
public static void enableBiometricEncryption(Context ctx, byte[] encryptedBiometricKey) {
public static void enableBiometricEncryption(Context ctx, BiometricKey biometricKey) {
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit()
.putBoolean("encryption.biometric", true)
.putString("encryption.biometric.key", Base64.encodeToString(encryptedBiometricKey, Base64.DEFAULT))
.putString("encryption.biometric.key", GSON.toJson(biometricKey))
.apply();
}
@ -141,21 +144,16 @@ public class SettingsUtil {
.apply();
}
public static byte[] getEncryptedBiometricKey(Context ctx) {
public static boolean isBiometricEncryption(Context ctx) {
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("encryption.biometric", false);
}
public static BiometricKey getBiometricKey(Context ctx) {
String encoded = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("encryption.biometric.key", null);
if(encoded == null) return null;
return Base64.decode(encoded, Base64.DEFAULT);
return GSON.fromJson(encoded, BiometricKey.class);
}
/*public static void setBiometricLock(Context ctx, boolean biometricLock) {
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean("biometricLock", biometricLock).apply();
}
public static boolean isBiometricLock(Context ctx) {
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("biometricLock", true);
}*/
public static void setEnableIntroVideo(Context ctx, boolean enableIntroVideo) {
SharedPreferences prefs = ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean("enableIntroVideo", enableIntroVideo).apply();

View File

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '8.1.0' apply false
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'com.google.protobuf' version '0.9.3' apply false
}