From bd7d3244e4b9ffcae9c33948a292fada15139f87 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 30 Jul 2024 08:08:00 +0200 Subject: [PATCH 1/7] Support GCM as credentials cryptor --- .../vault/RemoveStoredVaultPasswords.java | 3 +- .../util/BiometricAuthentication.kt | 3 +- .../util/CredentialCryptorTest.java | 19 +++++- .../cryptomator/util/KeyStoreBuilderTest.java | 18 +++++- .../{CipherImpl.java => BaseCipher.java} | 38 ++++-------- .../util/crypto/BiometricAuthCryptor.java | 25 +++++--- .../cryptomator/util/crypto/CipherCBC.java | 18 ++++++ .../cryptomator/util/crypto/CipherGCM.java | 20 +++++++ .../util/crypto/CredentialCryptor.java | 20 +++++-- .../util/crypto/CryptoByteArrayUtils.java | 17 ++++++ .../cryptomator/util/crypto/CryptoMode.java | 5 ++ .../util/crypto/CryptoOperations.java | 4 ++ ...ionsImpl.java => CryptoOperationsCBC.java} | 19 +++--- .../util/crypto/CryptoOperationsFactory.java | 31 ++++++---- .../util/crypto/CryptoOperationsGCM.java | 58 +++++++++++++++++++ .../util/crypto/KeyStoreBuilder.java | 13 ++--- 16 files changed, 237 insertions(+), 74 deletions(-) rename util/src/main/java/org/cryptomator/util/crypto/{CipherImpl.java => BaseCipher.java} (59%) create mode 100644 util/src/main/java/org/cryptomator/util/crypto/CipherCBC.java create mode 100644 util/src/main/java/org/cryptomator/util/crypto/CipherGCM.java create mode 100644 util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java create mode 100644 util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java rename util/src/main/java/org/cryptomator/util/crypto/{CryptoOperationsImpl.java => CryptoOperationsCBC.java} (74%) create mode 100644 util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsGCM.java diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java index b38f4ea69..28283f9c8 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java @@ -8,6 +8,7 @@ import org.cryptomator.generator.UseCase; import org.cryptomator.util.SharedPreferencesHandler; import org.cryptomator.util.crypto.BiometricAuthCryptor; +import org.cryptomator.util.crypto.CryptoMode; import static org.cryptomator.domain.Vault.aCopyOf; @@ -27,7 +28,7 @@ public RemoveStoredVaultPasswords(VaultRepository vaultRepository, // } public void execute() throws BackendException { - BiometricAuthCryptor.recreateKey(context); + BiometricAuthCryptor.recreateKey(context, CryptoMode.GCM); sharedPreferencesHandler.changeUseBiometricAuthentication(false); diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt index 241ddc9a0..341d533f5 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt @@ -9,6 +9,7 @@ import org.cryptomator.domain.Vault import org.cryptomator.presentation.R import org.cryptomator.presentation.model.VaultModel import org.cryptomator.util.crypto.BiometricAuthCryptor +import org.cryptomator.util.crypto.CryptoMode import org.cryptomator.util.crypto.UnrecoverableStorageKeyException import java.util.concurrent.Executor import javax.crypto.BadPaddingException @@ -45,7 +46,7 @@ class BiometricAuthentication(val callback: Callback, val context: Context, val val biometricAuthCryptor: BiometricAuthCryptor try { - biometricAuthCryptor = BiometricAuthCryptor.getInstance(context) + biometricAuthCryptor = BiometricAuthCryptor.getInstance(context, org.cryptomator.util.crypto.CryptoMode.GCM) } catch (e: UnrecoverableStorageKeyException) { return callback.onBiometricKeyInvalidated(vaultModel) } diff --git a/util/src/androidTest/java/org/cryptomator/util/CredentialCryptorTest.java b/util/src/androidTest/java/org/cryptomator/util/CredentialCryptorTest.java index 3599eeeb9..c4e7a3033 100644 --- a/util/src/androidTest/java/org/cryptomator/util/CredentialCryptorTest.java +++ b/util/src/androidTest/java/org/cryptomator/util/CredentialCryptorTest.java @@ -3,25 +3,38 @@ import android.content.Context; import androidx.test.filters.SmallTest; -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; import androidx.test.platform.app.InstrumentationRegistry; import org.cryptomator.util.crypto.CredentialCryptor; +import org.cryptomator.util.crypto.CryptoMode; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -@RunWith(AndroidJUnit4ClassRunner.class) +@RunWith(Parameterized.class) @SmallTest public class CredentialCryptorTest { private final byte[] decrypted = "lalala".getBytes(); + private final CryptoMode cryptoMode; private Context context; + public CredentialCryptorTest(CryptoMode cryptoMode) { + this.cryptoMode = cryptoMode; + } + + @Parameterized.Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] {{CryptoMode.GCM}, {CryptoMode.CBC}}); + } + @Before public void setup() { context = InstrumentationRegistry.getInstrumentation().getContext(); @@ -29,7 +42,7 @@ public void setup() { @Test public void testEncryptAndDecryptLeadsToSameDecryptedData() { - CredentialCryptor credentialCryptor = CredentialCryptor.getInstance(context); + CredentialCryptor credentialCryptor = CredentialCryptor.getInstance(context, cryptoMode); byte[] encrypted = credentialCryptor.encrypt(decrypted); assertThat(decrypted, is(credentialCryptor.decrypt(encrypted))); diff --git a/util/src/androidTest/java/org/cryptomator/util/KeyStoreBuilderTest.java b/util/src/androidTest/java/org/cryptomator/util/KeyStoreBuilderTest.java index c5284348b..e34d85696 100644 --- a/util/src/androidTest/java/org/cryptomator/util/KeyStoreBuilderTest.java +++ b/util/src/androidTest/java/org/cryptomator/util/KeyStoreBuilderTest.java @@ -3,26 +3,38 @@ import android.content.Context; import androidx.test.filters.SmallTest; -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; import androidx.test.platform.app.InstrumentationRegistry; +import org.cryptomator.util.crypto.CryptoMode; import org.cryptomator.util.crypto.KeyStoreBuilder; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.security.KeyStore; import java.security.KeyStoreException; +import java.util.Arrays; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -@RunWith(AndroidJUnit4ClassRunner.class) +@RunWith(Parameterized.class) @SmallTest public class KeyStoreBuilderTest { + private final CryptoMode cryptoMode; private Context context; + public KeyStoreBuilderTest(CryptoMode cryptoMode) { + this.cryptoMode = cryptoMode; + } + + @Parameterized.Parameters + public static Iterable data() { + return Arrays.asList(new Object[][] {{CryptoMode.GCM}, {CryptoMode.CBC}}); + } + @Before public void setup() { context = InstrumentationRegistry.getInstrumentation().getContext(); @@ -33,7 +45,7 @@ public void testAKeyStoreWithKeyLeadsToKeyInKeyStore() throws KeyStoreException String webdavKey = "webdavKey"; KeyStore inTestKeyStore = KeyStoreBuilder // .defaultKeyStore() // - .withKey(webdavKey, false, context) // + .withKey(webdavKey, false, cryptoMode, context) // .build(); assertThat(inTestKeyStore.containsAlias(webdavKey), is(true)); diff --git a/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java b/util/src/main/java/org/cryptomator/util/crypto/BaseCipher.java similarity index 59% rename from util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java rename to util/src/main/java/org/cryptomator/util/crypto/BaseCipher.java index e0432f3ee..976e4b14d 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CipherImpl.java +++ b/util/src/main/java/org/cryptomator/util/crypto/BaseCipher.java @@ -2,45 +2,32 @@ import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.spec.AlgorithmParameterSpec; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import static java.lang.System.arraycopy; - -class CipherImpl implements Cipher { - - private static final int IV_LENGTH = 16; +abstract class BaseCipher implements Cipher { private final javax.crypto.Cipher cipher; private final SecretKey key; + private final int ivLength; - CipherImpl(javax.crypto.Cipher cipher, SecretKey key) { + BaseCipher(javax.crypto.Cipher cipher, SecretKey key, int ivLength) { this.cipher = cipher; this.key = key; + this.ivLength = ivLength; } - private static byte[] mergeIvAndEncryptedData(byte[] encrypted, byte[] iv) { - byte[] mergedIvAndEncrypted = new byte[encrypted.length + iv.length]; - arraycopy(iv, 0, mergedIvAndEncrypted, 0, IV_LENGTH); - arraycopy(encrypted, 0, mergedIvAndEncrypted, IV_LENGTH, encrypted.length); - return mergedIvAndEncrypted; - } - - static byte[] getBytes(byte[] encryptedBytesWithIv) { - byte[] bytes = new byte[encryptedBytesWithIv.length - IV_LENGTH]; - arraycopy(encryptedBytesWithIv, IV_LENGTH, bytes, 0, bytes.length); - return bytes; - } + protected abstract AlgorithmParameterSpec getIvParameterSpec(byte[] iv); @Override public byte[] encrypt(byte[] data) { try { cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, key); byte[] encrypted = cipher.doFinal(data); - return mergeIvAndEncryptedData(encrypted, cipher.getIV()); + return CryptoByteArrayUtils.join(encrypted, cipher.getIV()); } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { throw new FatalCryptoException(e); } @@ -50,8 +37,8 @@ public byte[] encrypt(byte[] data) { public byte[] decrypt(byte[] encryptedBytesWithIv) { try { byte[] iv = getIv(encryptedBytesWithIv); - byte[] bytes = getBytes(encryptedBytesWithIv); - IvParameterSpec ivspec = new IvParameterSpec(iv); + byte[] bytes = CryptoByteArrayUtils.getBytes(encryptedBytesWithIv, ivLength); + AlgorithmParameterSpec ivspec = getIvParameterSpec(iv); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key, ivspec); return cipher.doFinal(bytes); } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { @@ -62,7 +49,7 @@ public byte[] decrypt(byte[] encryptedBytesWithIv) { @Override public javax.crypto.Cipher getDecryptCipher(byte[] encryptedBytesWithIv) throws InvalidAlgorithmParameterException, InvalidKeyException { byte[] iv = getIv(encryptedBytesWithIv); - IvParameterSpec ivspec = new IvParameterSpec(iv); + AlgorithmParameterSpec ivspec = getIvParameterSpec(iv); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key, ivspec); return cipher; } @@ -74,9 +61,8 @@ public javax.crypto.Cipher getEncryptCipher() throws InvalidKeyException { } private byte[] getIv(byte[] encryptedBytesWithIv) { - byte[] iv = new byte[IV_LENGTH]; - arraycopy(encryptedBytesWithIv, 0, iv, 0, IV_LENGTH); + byte[] iv = new byte[ivLength]; + System.arraycopy(encryptedBytesWithIv, 0, iv, 0, iv.length); return iv; } - } diff --git a/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java b/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java index deb77c427..ed57c1ff7 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java +++ b/util/src/main/java/org/cryptomator/util/crypto/BiometricAuthCryptor.java @@ -17,21 +17,29 @@ public class BiometricAuthCryptor { private static final String BIOMETRIC_AUTH_KEY_ALIAS = "fingerprintCryptoKeyAccessToken"; private final Cipher cipher; + private final CryptoMode cryptoMode; - private BiometricAuthCryptor(Context context) throws UnrecoverableStorageKeyException { + private BiometricAuthCryptor(Context context, CryptoMode cryptoMode) throws UnrecoverableStorageKeyException { + String suffixedAlias = getSuffixedAlias(cryptoMode); KeyStore keyStore = KeyStoreBuilder.defaultKeyStore() // - .withKey(BIOMETRIC_AUTH_KEY_ALIAS, true, context) // + .withKey(suffixedAlias, true, cryptoMode, context) // .build(); - this.cipher = CryptoOperationsFactory.cryptoOperations().cryptor(keyStore, BIOMETRIC_AUTH_KEY_ALIAS); + this.cryptoMode = cryptoMode; + this.cipher = CryptoOperationsFactory.cryptoOperations(cryptoMode).cryptor(keyStore, suffixedAlias); } - public static BiometricAuthCryptor getInstance(Context context) throws UnrecoverableStorageKeyException { - return new BiometricAuthCryptor(context); + private static String getSuffixedAlias(CryptoMode cryptoMode) { + // CBC does not have an alias due to legacy reasons + return cryptoMode == CryptoMode.GCM ? BIOMETRIC_AUTH_KEY_ALIAS + "_GCM" : BIOMETRIC_AUTH_KEY_ALIAS; } - public static void recreateKey(Context context) { + public static BiometricAuthCryptor getInstance(Context context, CryptoMode cryptoMode) throws UnrecoverableStorageKeyException { + return new BiometricAuthCryptor(context, cryptoMode); + } + + public static void recreateKey(Context context, CryptoMode cryptoMode) { KeyStoreBuilder.defaultKeyStore() // - .withRecreatedKey(BIOMETRIC_AUTH_KEY_ALIAS, true, context) // + .withRecreatedKey(getSuffixedAlias(cryptoMode), true, cryptoMode, context) // .build(); } @@ -50,7 +58,8 @@ public String encrypt(javax.crypto.Cipher cipher, String password) throws Illega } public String decrypt(javax.crypto.Cipher cipher, String password) throws IllegalBlockSizeException, BadPaddingException { - byte[] ciphered = cipher.doFinal(CipherImpl.getBytes(password.getBytes(StandardCharsets.ISO_8859_1))); + int ivLength = cryptoMode == CryptoMode.GCM ? CipherGCM.IV_LENGTH : CipherCBC.IV_LENGTH; + byte[] ciphered = cipher.doFinal(CryptoByteArrayUtils.getBytes(password.getBytes(StandardCharsets.ISO_8859_1), ivLength)); return new String(ciphered, StandardCharsets.UTF_8); } } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CipherCBC.java b/util/src/main/java/org/cryptomator/util/crypto/CipherCBC.java new file mode 100644 index 000000000..7d397d466 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/CipherCBC.java @@ -0,0 +1,18 @@ +package org.cryptomator.util.crypto; + +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +class CipherCBC extends BaseCipher { + + public static final int IV_LENGTH = 16; + + CipherCBC(javax.crypto.Cipher cipher, SecretKey key) { + super(cipher, key, IV_LENGTH); + } + + @Override + protected IvParameterSpec getIvParameterSpec(byte[] iv) { + return new IvParameterSpec(iv); + } +} diff --git a/util/src/main/java/org/cryptomator/util/crypto/CipherGCM.java b/util/src/main/java/org/cryptomator/util/crypto/CipherGCM.java new file mode 100644 index 000000000..8b2f14712 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/CipherGCM.java @@ -0,0 +1,20 @@ +package org.cryptomator.util.crypto; + +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +class CipherGCM extends BaseCipher { + + public static final int IV_LENGTH = 12; + + CipherGCM(javax.crypto.Cipher cipher, SecretKey key) { + super(cipher, key, IV_LENGTH); + } + + @Override + protected AlgorithmParameterSpec getIvParameterSpec(byte[] iv) { + return new GCMParameterSpec(128, iv); + } +} diff --git a/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java b/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java index 53712697c..c9fde89dd 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CredentialCryptor.java @@ -11,17 +11,27 @@ public class CredentialCryptor { private final Cipher cipher; - private CredentialCryptor(Context context) { + private static String getSuffixedAlias(CryptoMode cryptoMode) { + // CBC does not have an alias due to legacy reasons + return cryptoMode == CryptoMode.GCM ? CredentialCryptor.DEFAULT_KEY_ALIAS + "_GCM" : CredentialCryptor.DEFAULT_KEY_ALIAS; + } + + private CredentialCryptor(Context context, CryptoMode cryptoMode) { + String suffixedAlias = getSuffixedAlias(cryptoMode); KeyStore keyStore = KeyStoreBuilder.defaultKeyStore() // - .withKey(DEFAULT_KEY_ALIAS, false, context) // + .withKey(suffixedAlias, false, cryptoMode, context) // .build(); this.cipher = CryptoOperationsFactory // - .cryptoOperations() // - .cryptor(keyStore, DEFAULT_KEY_ALIAS); + .cryptoOperations(cryptoMode) // + .cryptor(keyStore, suffixedAlias); } public static CredentialCryptor getInstance(Context context) { - return new CredentialCryptor(context); + return new CredentialCryptor(context, CryptoMode.GCM); + } + + public static CredentialCryptor getInstance(Context context, CryptoMode cryptoMode) { + return new CredentialCryptor(context, cryptoMode); } public byte[] encrypt(byte[] decrypted) { diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java new file mode 100644 index 000000000..099389318 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java @@ -0,0 +1,17 @@ +package org.cryptomator.util.crypto; + +public class CryptoByteArrayUtils { + + public static byte[] getBytes(byte[] encryptedBytesWithIv, int ivLength) { + byte[] bytes = new byte[encryptedBytesWithIv.length - ivLength]; + System.arraycopy(encryptedBytesWithIv, ivLength, bytes, 0, bytes.length); + return bytes; + } + + public static byte[] join(byte[] encrypted, byte[] iv) { + byte[] result = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(encrypted, 0, result, iv.length, encrypted.length); + return result; + } +} diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java new file mode 100644 index 000000000..0e57bef20 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java @@ -0,0 +1,5 @@ +package org.cryptomator.util.crypto; + +public enum CryptoMode { + CBC, GCM; +} diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperations.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperations.java index 06cf7d7d0..f04f1c318 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperations.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperations.java @@ -1,11 +1,15 @@ package org.cryptomator.util.crypto; import android.content.Context; +import android.security.keystore.KeyProperties; import java.security.KeyStore; interface CryptoOperations { + String ANDROID_KEYSTORE = KeyStoreBuilder.DEFAULT_KEYSTORE_NAME; + String ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES; + KeyGenerator initializeKeyGenerator(Context context, String alias); Cipher cryptor(KeyStore keyStore, String alias); diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsCBC.java similarity index 74% rename from util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java rename to util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsCBC.java index 04acdceb1..135e69f2f 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsImpl.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsCBC.java @@ -13,16 +13,19 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; -class CryptoOperationsImpl implements CryptoOperations { +class CryptoOperationsCBC implements CryptoOperations { + + private static final String ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC; + private static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7; @Override public Cipher cryptor(KeyStore keyStore, String alias) throws UnrecoverableStorageKeyException { try { final SecretKey key = (SecretKey) keyStore.getKey(alias, null); - final javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" // - + KeyProperties.BLOCK_MODE_CBC + "/" // - + KeyProperties.ENCRYPTION_PADDING_PKCS7); - return new CipherImpl(cipher, key); + final javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(ENCRYPTION_ALGORITHM + "/" // + + ENCRYPTION_BLOCK_MODE + "/" // + + ENCRYPTION_PADDING); + return new CipherCBC(cipher, key); } catch (UnrecoverableKeyException e) { throw new UnrecoverableStorageKeyException(e); } catch (NoSuchPaddingException | NoSuchAlgorithmException | KeyStoreException e) { @@ -34,15 +37,15 @@ public Cipher cryptor(KeyStore keyStore, String alias) throws UnrecoverableStora public KeyGenerator initializeKeyGenerator(Context context, final String alias) { final javax.crypto.KeyGenerator generator; try { - generator = javax.crypto.KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KeyStoreBuilder.DEFAULT_KEYSTORE_NAME); + generator = javax.crypto.KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEYSTORE); } catch (NoSuchAlgorithmException | NoSuchProviderException e) { throw new FatalCryptoException(e); } return requireUserAuthentication -> { KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec // .Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) // - .setBlockModes(KeyProperties.BLOCK_MODE_CBC) // - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) // + .setBlockModes(ENCRYPTION_BLOCK_MODE) // + .setEncryptionPaddings(ENCRYPTION_PADDING) // .setUserAuthenticationRequired(requireUserAuthentication) // .setInvalidatedByBiometricEnrollment(requireUserAuthentication); diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java index e06507b9e..da7722af5 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsFactory.java @@ -2,21 +2,28 @@ class CryptoOperationsFactory { - private static volatile CryptoOperations cryptoOperations; + private static volatile CryptoOperations cryptoOperationsCBC; + private static volatile CryptoOperations cryptoOperationsGCM; - public static CryptoOperations cryptoOperations() { - if (cryptoOperations == null) { - synchronized (CryptoOperations.class) { - if (cryptoOperations == null) { - cryptoOperations = createCryptoOperations(); + public static CryptoOperations cryptoOperations(CryptoMode mode) { + if (mode == CryptoMode.CBC) { + if (cryptoOperationsCBC == null) { + synchronized (CryptoOperations.class) { + if (cryptoOperationsCBC == null) { + cryptoOperationsCBC = new CryptoOperationsCBC(); + } } } + return cryptoOperationsCBC; + } else { + if (cryptoOperationsGCM == null) { + synchronized (CryptoOperations.class) { + if (cryptoOperationsGCM == null) { + cryptoOperationsGCM = new CryptoOperationsGCM(); + } + } + } + return cryptoOperationsGCM; } - return cryptoOperations; } - - private static CryptoOperations createCryptoOperations() { - return new CryptoOperationsImpl(); - } - } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsGCM.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsGCM.java new file mode 100644 index 000000000..63c4aec29 --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoOperationsGCM.java @@ -0,0 +1,58 @@ +package org.cryptomator.util.crypto; + +import android.content.Context; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; + +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +class CryptoOperationsGCM implements CryptoOperations { + + private static final int KEY_SIZE = 256; + private static final String ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM; + private static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE; + + @Override + public Cipher cryptor(KeyStore keyStore, String alias) throws UnrecoverableStorageKeyException { + try { + final SecretKey key = (SecretKey) keyStore.getKey(alias, null); + final javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(ENCRYPTION_ALGORITHM + "/" // + + ENCRYPTION_BLOCK_MODE + "/" // + + ENCRYPTION_PADDING); + return new CipherGCM(cipher, key); + } catch (UnrecoverableKeyException e) { + throw new UnrecoverableStorageKeyException(e); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | KeyStoreException e) { + throw new FatalCryptoException(e); + } + } + + @Override + public KeyGenerator initializeKeyGenerator(Context context, final String alias) { + final javax.crypto.KeyGenerator generator; + try { + generator = javax.crypto.KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEYSTORE); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new FatalCryptoException(e); + } + return requireUserAuthentication -> { + KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec // + .Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) // + .setBlockModes(ENCRYPTION_BLOCK_MODE) // + .setEncryptionPaddings(ENCRYPTION_PADDING) // + .setKeySize(KEY_SIZE) // + .setUserAuthenticationRequired(requireUserAuthentication) // + .setInvalidatedByBiometricEnrollment(requireUserAuthentication); + + generator.init(builder.build()); + generator.generateKey(); + }; + } +} diff --git a/util/src/main/java/org/cryptomator/util/crypto/KeyStoreBuilder.java b/util/src/main/java/org/cryptomator/util/crypto/KeyStoreBuilder.java index c87e51108..079448bf9 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/KeyStoreBuilder.java +++ b/util/src/main/java/org/cryptomator/util/crypto/KeyStoreBuilder.java @@ -44,9 +44,9 @@ public interface CustomKeyStoreBuilder { public interface DefaultKeyStoreBuilder extends CustomKeyStoreBuilder { - DefaultKeyStoreBuilder withKey(String alias, boolean requireUserAuthentication, Context context); + DefaultKeyStoreBuilder withKey(String alias, boolean requireUserAuthentication, CryptoMode mode, Context context); - CustomKeyStoreBuilder withRecreatedKey(String alias, boolean requireUserAuthentication, Context context); + CustomKeyStoreBuilder withRecreatedKey(String alias, boolean requireUserAuthentication, CryptoMode mode, Context context); } private static class KeyStoreBuilderImpl implements KeyStoreBuilder.CustomKeyStoreBuilder, KeyStoreBuilder.DefaultKeyStoreBuilder { @@ -57,10 +57,10 @@ private KeyStoreBuilderImpl(KeyStore keyStore) { this.keyStore = keyStore; } - public KeyStoreBuilderImpl withKey(String alias, boolean requireUserAuthentication, Context context) { + public KeyStoreBuilderImpl withKey(String alias, boolean requireUserAuthentication, CryptoMode mode, Context context) { try { if (!doesKeyExist(alias)) { - CryptoOperationsFactory.cryptoOperations() // + CryptoOperationsFactory.cryptoOperations(mode) // .initializeKeyGenerator(context, alias) // .createKey(requireUserAuthentication); } @@ -70,11 +70,10 @@ public KeyStoreBuilderImpl withKey(String alias, boolean requireUserAuthenticati return this; } - public CustomKeyStoreBuilder withRecreatedKey(String alias, boolean requireUserAuthentication, Context context) { + public CustomKeyStoreBuilder withRecreatedKey(String alias, boolean requireUserAuthentication, CryptoMode mode, Context context) { try { keyStore.deleteEntry(alias); - - CryptoOperationsFactory.cryptoOperations() // + CryptoOperationsFactory.cryptoOperations(mode) // .initializeKeyGenerator(context, alias) // .createKey(requireUserAuthentication); } catch (Exception e) { From ac5f3ff2eeef0cea7210c160ffe65d75b8f64ea5 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 31 Jul 2024 10:19:23 +0200 Subject: [PATCH 2/7] Migrate access tokens to GCM --- data/build.gradle | 2 +- .../data/db/UpgradeDatabaseTest.kt | 118 +++++++++++++++++ .../cryptomator/data/db/DatabaseUpgrades.java | 6 +- .../java/org/cryptomator/data/db/Sql.java | 4 + .../org/cryptomator/data/db/Upgrade12To13.kt | 125 ++++++++++++++++++ .../data/db/entities/CloudEntity.java | 27 +++- .../data/db/entities/VaultEntity.java | 17 ++- 7 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt diff --git a/data/build.gradle b/data/build.gradle index f28de6329..4e3014675 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -90,7 +90,7 @@ android { } greendao { - schemaVersion 12 + schemaVersion 13 } configurations.all { diff --git a/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt b/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt index d7d5a7ec6..d72e0ce6b 100644 --- a/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt +++ b/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt @@ -12,6 +12,7 @@ import org.cryptomator.data.db.entities.VaultEntityDao import org.cryptomator.domain.CloudType import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.crypto.CryptoMode import org.greenrobot.greendao.database.Database import org.greenrobot.greendao.database.StandardDatabase import org.greenrobot.greendao.internal.DaoConfig @@ -643,4 +644,121 @@ class UpgradeDatabaseTest { Assert.assertThat(sharedPreferencesHandler.updateIntervalInDays(), CoreMatchers.`is`(Optional.absent())) } + + @Test + fun upgrade12To13() { + Upgrade0To1().applyTo(db, 0) + Upgrade1To2().applyTo(db, 1) + Upgrade2To3(context).applyTo(db, 2) + Upgrade3To4().applyTo(db, 3) + Upgrade4To5().applyTo(db, 4) + Upgrade5To6().applyTo(db, 5) + Upgrade6To7().applyTo(db, 6) + Upgrade7To8().applyTo(db, 7) + Upgrade8To9(sharedPreferencesHandler).applyTo(db, 8) + Upgrade9To10(sharedPreferencesHandler).applyTo(db, 9) + Upgrade10To11().applyTo(db, 10) + Upgrade11To12(sharedPreferencesHandler).applyTo(db, 11) + + val gcmCryptor = CredentialCryptor.getInstance(context, CryptoMode.GCM) + val cbcCryptor = CredentialCryptor.getInstance(context, CryptoMode.CBC) + + val accessTokenPlain = "accessToken" + val accessTokenCiphertext = cbcCryptor.encrypt(accessTokenPlain) + val s3SecretPlain = "s3SecretKey" + val s3SecretCiphertext = cbcCryptor.encrypt(s3SecretPlain) + val vaultPasswordPlain = "password" + + Sql.insertInto("VAULT_ENTITY") // + .integer("_id", 25) // + .integer("FOLDER_CLOUD_ID", 15) // + .text("FOLDER_PATH", "path") // + .text("FOLDER_NAME", "name") // + .text("CLOUD_TYPE", CloudType.LOCAL.name) // + .text("PASSWORD", "password") // + .integer("POSITION", 10) // + .integer("FORMAT", 8) // + .integer("SHORTENING_THRESHOLD", 4) + .executeOn(db) + + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 15) // + .text("TYPE", CloudType.LOCAL.name) // + .text("URL", "url") // + .text("USERNAME", "username") // + .text("WEBDAV_CERTIFICATE", "certificate") // + .text("ACCESS_TOKEN", accessTokenCiphertext) + .text("S3_BUCKET", "s3Bucket") // + .text("S3_REGION", "s3Region") // + .text("S3_SECRET_KEY", s3SecretCiphertext) // + .executeOn(db) + + Sql.insertInto("VAULT_ENTITY") // + .integer("_id", 3025) // + .integer("FOLDER_CLOUD_ID", 3015) // + .text("FOLDER_PATH", "path") // + .text("FOLDER_NAME", "name") // + .text("CLOUD_TYPE", CloudType.LOCAL.name) // + .text("PASSWORD", null) // + .integer("POSITION", 10) // + .integer("FORMAT", 8) // + .integer("SHORTENING_THRESHOLD", 4) + .executeOn(db) + + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 3015) // + .text("TYPE", CloudType.LOCAL.name) // + .text("URL", "url") // + .text("USERNAME", "username") // + .text("WEBDAV_CERTIFICATE", "certificate") // + .text("ACCESS_TOKEN", null) + .text("S3_BUCKET", "s3Bucket") // + .text("S3_REGION", "s3Region") // + .text("S3_SECRET_KEY", null) // + .executeOn(db) + + Upgrade12To13(context).applyTo(db, 12) + + Sql.query("VAULT_ENTITY").where("_id", Sql.eq(25)).executeOn(db).use { + it.moveToFirst() + Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD")), CoreMatchers.`is`(vaultPasswordPlain)) + Assert.assertThat(it.getString(it.getColumnIndex("PASSWORD_CRYPTO_MODE")), CoreMatchers.`is`(CryptoMode.CBC.name)) + + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`("path")) + Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`("name")) + Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.LOCAL.name)) + Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(10)) + Assert.assertThat(it.getInt(it.getColumnIndex("FORMAT")), CoreMatchers.`is`(8)) + Assert.assertThat(it.getInt(it.getColumnIndex("SHORTENING_THRESHOLD")), CoreMatchers.`is`(4)) + } + + Sql.query("CLOUD_ENTITY").where("_id", Sql.eq(15)).executeOn(db).use { + it.moveToFirst() + Assert.assertThat(gcmCryptor.decrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN"))), CoreMatchers.`is`(accessTokenPlain)) + Assert.assertThat(it.getString(it.getColumnIndex("ACCESS_TOKEN_CRYPTO_MODE")), CoreMatchers.`is`(CryptoMode.GCM.name)) + Assert.assertThat(gcmCryptor.decrypt(it.getString(it.getColumnIndex("S3_SECRET_KEY"))), CoreMatchers.`is`(s3SecretPlain)) + Assert.assertThat(it.getString(it.getColumnIndex("S3_SECRET_KEY_CRYPTO_MODE")), CoreMatchers.`is`(CryptoMode.GCM.name)) + + Assert.assertThat(it.getString(it.getColumnIndex("TYPE")), CoreMatchers.`is`(CloudType.LOCAL.name)) + Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`("url")) + Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`("username")) + Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`("certificate")) + Assert.assertThat(it.getString(it.getColumnIndex("S3_BUCKET")), CoreMatchers.`is`("s3Bucket")) + Assert.assertThat(it.getString(it.getColumnIndex("S3_REGION")), CoreMatchers.`is`("s3Region")) + } + + Sql.query("VAULT_ENTITY").where("_id", Sql.eq(3025)).executeOn(db).use { + it.moveToFirst() + Assert.assertNull(it.getString(it.getColumnIndex("PASSWORD"))) + Assert.assertNull(it.getString(it.getColumnIndex("PASSWORD_CRYPTO_MODE"))) + } + + Sql.query("CLOUD_ENTITY").where("_id", Sql.eq(3015)).executeOn(db).use { + it.moveToFirst() + Assert.assertNull(it.getString(it.getColumnIndex("ACCESS_TOKEN"))) + Assert.assertNull(it.getString(it.getColumnIndex("ACCESS_TOKEN_CRYPTO_MODE"))) + Assert.assertNull(it.getString(it.getColumnIndex("S3_SECRET_KEY"))) + Assert.assertNull(it.getString(it.getColumnIndex("S3_SECRET_KEY_CRYPTO_MODE"))) + } + } } diff --git a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java index 63566c5b8..52401e64f 100644 --- a/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java +++ b/data/src/main/java/org/cryptomator/data/db/DatabaseUpgrades.java @@ -30,7 +30,8 @@ public DatabaseUpgrades( // Upgrade8To9 upgrade8To9, // Upgrade9To10 upgrade9To10, // Upgrade10To11 upgrade10To11, // - Upgrade11To12 upgrade11To12 + Upgrade11To12 upgrade11To12, // + Upgrade12To13 upgrade12To13 ) { availableUpgrades = defineUpgrades( // @@ -45,7 +46,8 @@ public DatabaseUpgrades( // upgrade8To9, // upgrade9To10, // upgrade10To11, // - upgrade11To12); + upgrade11To12, // + upgrade12To13); } private Map> defineUpgrades(DatabaseUpgrade... upgrades) { diff --git a/data/src/main/java/org/cryptomator/data/db/Sql.java b/data/src/main/java/org/cryptomator/data/db/Sql.java index 9c50cc4bc..1d961e146 100644 --- a/data/src/main/java/org/cryptomator/data/db/Sql.java +++ b/data/src/main/java/org/cryptomator/data/db/Sql.java @@ -65,6 +65,10 @@ public static Criterion isNull() { return (column, whereClause, whereArgs) -> whereClause.append('"').append(column).append("\" IS NULL"); } + public static Criterion isNotNull() { + return (column, whereClause, whereArgs) -> whereClause.append('"').append(column).append("\" IS NOT NULL"); + } + public static Criterion eq(final Long value) { return (column, whereClause, whereArgs) -> whereClause.append('"').append(column).append("\" = ").append(value); } diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt new file mode 100644 index 000000000..dfdc63c26 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt @@ -0,0 +1,125 @@ +package org.cryptomator.data.db + +import android.content.Context +import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CredentialCryptor +import org.cryptomator.util.crypto.CryptoMode +import org.greenrobot.greendao.database.Database +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class Upgrade12To13 @Inject constructor(private val context: Context) : DatabaseUpgrade(12, 13) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + addCryptoModeToDbEntities(db) + addDefaultPasswordCryptoModeToDb(db) + upgradeCloudCryptoModeToGCM(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + private fun addCryptoModeToDbEntities(db: Database) { + Sql.alterTable("CLOUD_ENTITY").renameTo("CLOUD_ENTITY_OLD").executeOn(db) + + Sql.createTable("CLOUD_ENTITY") // + .id() // + .requiredText("TYPE") // + .optionalText("ACCESS_TOKEN") // + .optionalText("ACCESS_TOKEN_CRYPTO_MODE") // + .optionalText("URL") // + .optionalText("USERNAME") // + .optionalText("WEBDAV_CERTIFICATE") // + .optionalText("S3_BUCKET") // + .optionalText("S3_REGION") // + .optionalText("S3_SECRET_KEY") // + .optionalText("S3_SECRET_KEY_CRYPTO_MODE") // + .executeOn(db) + + Sql.insertInto("CLOUD_ENTITY") // + .select("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE", "S3_BUCKET", "S3_REGION", "S3_SECRET_KEY") // + .columns("_id", "TYPE", "ACCESS_TOKEN", "URL", "USERNAME", "WEBDAV_CERTIFICATE", "S3_BUCKET", "S3_REGION", "S3_SECRET_KEY") // + .from("CLOUD_ENTITY_OLD") // + .executeOn(db) + + // use this to recreate the index but also add the new column as well + addPasswordCryptoModeToVaultDbEntity(db) + + Sql.dropTable("CLOUD_ENTITY_OLD").executeOn(db) + } + + private fun addPasswordCryptoModeToVaultDbEntity(db: Database) { + Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db) + Sql.createTable("VAULT_ENTITY") // + .id() // + .optionalInt("FOLDER_CLOUD_ID") // + .optionalText("FOLDER_PATH") // + .optionalText("FOLDER_NAME") // + .optionalInt("FORMAT") // + .requiredText("CLOUD_TYPE") // + .optionalText("PASSWORD") // + .optionalText("PASSWORD_CRYPTO_MODE") // + .optionalInt("POSITION") // + .optionalInt("SHORTENING_THRESHOLD") // + .foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", Sql.SqlCreateTableBuilder.ForeignKeyBehaviour.ON_DELETE_SET_NULL) // + .executeOn(db) + + Sql.insertInto("VAULT_ENTITY") // + .select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "FORMAT", "PASSWORD", "POSITION", "SHORTENING_THRESHOLD", "CLOUD_ENTITY.TYPE") // + .columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "FORMAT", "PASSWORD", "POSITION", "SHORTENING_THRESHOLD", "CLOUD_TYPE") // + .from("VAULT_ENTITY_OLD") // + .join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db) + + Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") // + .on("VAULT_ENTITY") // + .asc("FOLDER_PATH") // + .asc("FOLDER_CLOUD_ID") // + .executeOn(db) + + Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db) + } + + private fun addDefaultPasswordCryptoModeToDb(db: Database) { + Sql.update("VAULT_ENTITY") // + .where("PASSWORD", Sql.isNotNull()) + .set("PASSWORD_CRYPTO_MODE", Sql.toString(CryptoMode.CBC.name)) // + .executeOn(db) + } + + private fun upgradeCloudCryptoModeToGCM(db: Database) { + val gcmCryptor = CredentialCryptor.getInstance(context, CryptoMode.GCM) + val cbcCryptor = CredentialCryptor.getInstance(context, CryptoMode.CBC) + + Sql.query("CLOUD_ENTITY").where("ACCESS_TOKEN", Sql.isNotNull()).executeOn(db).use { + while (it.moveToNext()) { + Sql.update("CLOUD_ENTITY") + .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) + .set("ACCESS_TOKEN", Sql.toString(reEncrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN")), gcmCryptor, cbcCryptor))) + .set("ACCESS_TOKEN_CRYPTO_MODE", Sql.toString(CryptoMode.GCM.name)) + .executeOn(db) + } + } + Sql.query("CLOUD_ENTITY").where("S3_SECRET_KEY", Sql.isNotNull()).executeOn(db).use { + while (it.moveToNext()) { + Sql.update("CLOUD_ENTITY") + .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) + .set("S3_SECRET_KEY", Sql.toString(reEncrypt(it.getString(it.getColumnIndex("S3_SECRET_KEY")), gcmCryptor, cbcCryptor))) + .set("S3_SECRET_KEY_CRYPTO_MODE", Sql.toString(CryptoMode.GCM.name)) + .executeOn(db) + } + } + } + + private fun reEncrypt(ciphertext: String?, gcmCryptor: CredentialCryptor, cbcCryptor: CredentialCryptor): String? { + if (ciphertext == null) return null + val accessToken = cbcCryptor.decrypt(ciphertext) + return gcmCryptor.encrypt(accessToken) + } +} diff --git a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java index b939e9251..385898bc9 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/CloudEntity.java @@ -16,6 +16,8 @@ public class CloudEntity extends DatabaseEntity { private String accessToken; + private String accessTokenCryptoMode; + private String url; private String username; @@ -28,17 +30,22 @@ public class CloudEntity extends DatabaseEntity { private String s3SecretKey; - @Generated(hash = 1685351705) - public CloudEntity(Long id, @NotNull String type, String accessToken, String url, String username, String webdavCertificate, String s3Bucket, String s3Region, String s3SecretKey) { + private String s3SecretKeyCryptoMode; + + @Generated(hash = 930663276) + public CloudEntity(Long id, @NotNull String type, String accessToken, String accessTokenCryptoMode, String url, String username, String webdavCertificate, String s3Bucket, + String s3Region, String s3SecretKey, String s3SecretKeyCryptoMode) { this.id = id; this.type = type; this.accessToken = accessToken; + this.accessTokenCryptoMode = accessTokenCryptoMode; this.url = url; this.username = username; this.webdavCertificate = webdavCertificate; this.s3Bucket = s3Bucket; this.s3Region = s3Region; this.s3SecretKey = s3SecretKey; + this.s3SecretKeyCryptoMode = s3SecretKeyCryptoMode; } @Generated(hash = 1354152224) @@ -116,4 +123,20 @@ public String getS3SecretKey() { public void setS3SecretKey(String s3SecretKey) { this.s3SecretKey = s3SecretKey; } + + public String getAccessTokenCryptoMode() { + return this.accessTokenCryptoMode; + } + + public void setAccessTokenCryptoMode(String accessTokenCryptoMode) { + this.accessTokenCryptoMode = accessTokenCryptoMode; + } + + public String getS3SecretKeyCryptoMode() { + return this.s3SecretKeyCryptoMode; + } + + public void setS3SecretKeyCryptoMode(String s3SecretKeyCryptoMode) { + this.s3SecretKeyCryptoMode = s3SecretKeyCryptoMode; + } } diff --git a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java index 9f384b3c6..e23220c60 100644 --- a/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java +++ b/data/src/main/java/org/cryptomator/data/db/entities/VaultEntity.java @@ -28,6 +28,8 @@ public class VaultEntity extends DatabaseEntity { private String password; + private String passwordCryptoMode; + private Integer position; private Integer format; @@ -44,17 +46,20 @@ public class VaultEntity extends DatabaseEntity { */ @Generated(hash = 2040040024) private transient DaoSession daoSession; + @Generated(hash = 229273163) private transient Long folderCloud__resolvedKey; - @Generated(hash = 530735379) - public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password, Integer position, Integer format, Integer shorteningThreshold) { + @Generated(hash = 1663458645) + public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password, String passwordCryptoMode, Integer position, Integer format, + Integer shorteningThreshold) { this.id = id; this.folderCloudId = folderCloudId; this.folderPath = folderPath; this.folderName = folderName; this.cloudType = cloudType; this.password = password; + this.passwordCryptoMode = passwordCryptoMode; this.position = position; this.format = format; this.shorteningThreshold = shorteningThreshold; @@ -205,6 +210,14 @@ public void setShorteningThreshold(Integer shorteningThreshold) { this.shorteningThreshold = shorteningThreshold; } + public String getPasswordCryptoMode() { + return this.passwordCryptoMode; + } + + public void setPasswordCryptoMode(String passwordCryptoMode) { + this.passwordCryptoMode = passwordCryptoMode; + } + /** called by internal mechanisms, do not call yourself. */ @Generated(hash = 674742652) public void __setDaoSession(DaoSession daoSession) { From 5af0cdbfe7345aca51f4ad46d23eb3458aea7cd2 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 6 Aug 2024 18:16:50 +0200 Subject: [PATCH 3/7] WIP vault password gcm migration --- .../org/cryptomator/data/db/Upgrade12To13.kt | 33 ++-- .../data/db/mappers/VaultEntityMapper.java | 10 +- .../java/org/cryptomator/domain/Vault.java | 20 ++- .../vault/ListCBCEncryptedPasswordVaults.java | 29 ++++ .../vault/RemoveStoredVaultPasswords.java | 26 +-- ...VaultPasswordsAndDisableBiometricAuth.java | 44 +++++ .../domain/usecases/vault/SaveVaults.java | 31 ++++ .../presentation/model/VaultModel.kt | 3 + .../BiometricAuthSettingsPresenter.kt | 7 +- .../presenter/UnlockVaultPresenter.kt | 11 +- .../presenter/VaultListPresenter.kt | 106 ++++++++++-- .../ui/activity/UnlockVaultActivity.kt | 6 +- .../ui/activity/VaultListActivity.kt | 35 +++- .../ui/activity/view/VaultListView.kt | 1 + .../CBCPasswordVaultsMigrationDialog.kt | 46 +++++ .../util/BiometricAuthentication.kt | 3 +- .../util/BiometricAuthenticationMigration.kt | 161 ++++++++++++++++++ .../dialog_cbc_password_vaults_migration.xml | 18 ++ presentation/src/main/res/values/strings.xml | 5 + .../presenter/VaultListPresenterTest.java | 9 + .../cryptomator/util/crypto/CryptoMode.java | 2 +- 21 files changed, 543 insertions(+), 63 deletions(-) create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/vault/ListCBCEncryptedPasswordVaults.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswordsAndDisableBiometricAuth.java create mode 100644 domain/src/main/java/org/cryptomator/domain/usecases/vault/SaveVaults.java create mode 100644 presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt create mode 100644 presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt create mode 100644 presentation/src/main/res/layout/dialog_cbc_password_vaults_migration.xml diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt index dfdc63c26..bc576c8b3 100644 --- a/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt @@ -7,6 +7,7 @@ import org.cryptomator.util.crypto.CryptoMode import org.greenrobot.greendao.database.Database import javax.inject.Inject import javax.inject.Singleton +import timber.log.Timber @Singleton internal class Upgrade12To13 @Inject constructor(private val context: Context) : DatabaseUpgrade(12, 13) { @@ -15,7 +16,7 @@ internal class Upgrade12To13 @Inject constructor(private val context: Context) : db.beginTransaction() try { addCryptoModeToDbEntities(db) - addDefaultPasswordCryptoModeToDb(db) + applyVaultPasswordCryptoModeToDb(db) upgradeCloudCryptoModeToGCM(db) db.setTransactionSuccessful() } finally { @@ -86,11 +87,15 @@ internal class Upgrade12To13 @Inject constructor(private val context: Context) : Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db) } - private fun addDefaultPasswordCryptoModeToDb(db: Database) { - Sql.update("VAULT_ENTITY") // - .where("PASSWORD", Sql.isNotNull()) - .set("PASSWORD_CRYPTO_MODE", Sql.toString(CryptoMode.CBC.name)) // - .executeOn(db) + private fun applyVaultPasswordCryptoModeToDb(db: Database) { + Sql.query("VAULT_ENTITY").where("PASSWORD", Sql.isNotNull()).executeOn(db).use { + while (it.moveToNext()) { + Sql.update("VAULT_ENTITY") // + .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) // + .set("PASSWORD_CRYPTO_MODE", Sql.toString(CryptoMode.CBC.toString())) // + .executeOn(db) + } + } } private fun upgradeCloudCryptoModeToGCM(db: Database) { @@ -99,19 +104,19 @@ internal class Upgrade12To13 @Inject constructor(private val context: Context) : Sql.query("CLOUD_ENTITY").where("ACCESS_TOKEN", Sql.isNotNull()).executeOn(db).use { while (it.moveToNext()) { - Sql.update("CLOUD_ENTITY") - .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) - .set("ACCESS_TOKEN", Sql.toString(reEncrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN")), gcmCryptor, cbcCryptor))) - .set("ACCESS_TOKEN_CRYPTO_MODE", Sql.toString(CryptoMode.GCM.name)) + Sql.update("CLOUD_ENTITY") // + .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) // + .set("ACCESS_TOKEN", Sql.toString(reEncrypt(it.getString(it.getColumnIndex("ACCESS_TOKEN")), gcmCryptor, cbcCryptor))) // + .set("ACCESS_TOKEN_CRYPTO_MODE", Sql.toString(CryptoMode.GCM.toString())) // .executeOn(db) } } Sql.query("CLOUD_ENTITY").where("S3_SECRET_KEY", Sql.isNotNull()).executeOn(db).use { while (it.moveToNext()) { - Sql.update("CLOUD_ENTITY") - .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) - .set("S3_SECRET_KEY", Sql.toString(reEncrypt(it.getString(it.getColumnIndex("S3_SECRET_KEY")), gcmCryptor, cbcCryptor))) - .set("S3_SECRET_KEY_CRYPTO_MODE", Sql.toString(CryptoMode.GCM.name)) + Sql.update("CLOUD_ENTITY") // + .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) // + .set("S3_SECRET_KEY", Sql.toString(reEncrypt(it.getString(it.getColumnIndex("S3_SECRET_KEY")), gcmCryptor, cbcCryptor))) // + .set("S3_SECRET_KEY_CRYPTO_MODE", Sql.toString(CryptoMode.GCM.toString())) // .executeOn(db) } } diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java index 2659922d7..3cd19471f 100644 --- a/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java +++ b/data/src/main/java/org/cryptomator/data/db/mappers/VaultEntityMapper.java @@ -5,6 +5,7 @@ import org.cryptomator.domain.CloudType; import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.util.crypto.CryptoMode; import javax.inject.Inject; import javax.inject.Singleton; @@ -29,7 +30,7 @@ public Vault fromEntity(VaultEntity entity) throws BackendException { .withPath(entity.getFolderPath()) // .withCloud(cloudFrom(entity)) // .withCloudType(CloudType.valueOf(entity.getCloudType())) // - .withSavedPassword(entity.getPassword()) // + .withSavedPassword(entity.getPassword(), cryptoModeFrom(entity)) // .withPosition(entity.getPosition()) // .withFormat(entity.getFormat()) // .withShorteningThreshold(entity.getShorteningThreshold()) // @@ -43,6 +44,10 @@ private Cloud cloudFrom(VaultEntity entity) { return cloudEntityMapper.fromEntity(entity.getFolderCloud()); } + private CryptoMode cryptoModeFrom(VaultEntity entity) { + return entity.getPasswordCryptoMode() != null ? CryptoMode.valueOf(entity.getPasswordCryptoMode()) : null; + } + @Override public VaultEntity toEntity(Vault domainObject) { VaultEntity entity = new VaultEntity(); @@ -54,6 +59,9 @@ public VaultEntity toEntity(Vault domainObject) { } entity.setCloudType(domainObject.getCloudType().name()); entity.setPassword(domainObject.getPassword()); + if (domainObject.getPasswordCryptoMode() != null) { + entity.setPasswordCryptoMode(domainObject.getPasswordCryptoMode().name()); + } entity.setPosition(domainObject.getPosition()); entity.setFormat(domainObject.getFormat()); entity.setShorteningThreshold(domainObject.getShorteningThreshold()); diff --git a/domain/src/main/java/org/cryptomator/domain/Vault.java b/domain/src/main/java/org/cryptomator/domain/Vault.java index de66a6a76..6be3fb4ea 100644 --- a/domain/src/main/java/org/cryptomator/domain/Vault.java +++ b/domain/src/main/java/org/cryptomator/domain/Vault.java @@ -1,5 +1,7 @@ package org.cryptomator.domain; +import org.cryptomator.util.crypto.CryptoMode; + import java.io.Serializable; public class Vault implements Serializable { @@ -12,6 +14,7 @@ public class Vault implements Serializable { private final CloudType cloudType; private final boolean unlocked; private final String password; + private final CryptoMode passwordCryptoMode; private final int format; private final int shorteningThreshold; private final int position; @@ -24,6 +27,7 @@ private Vault(Builder builder) { this.unlocked = builder.unlocked; this.cloudType = builder.cloudType; this.password = builder.password; + this.passwordCryptoMode = builder.passwordCryptoMode; this.format = builder.format; this.shorteningThreshold = builder.shorteningThreshold; this.position = builder.position; @@ -41,7 +45,7 @@ public static Builder aCopyOf(Vault vault) { .withName(vault.getName()) // .withPath(vault.getPath()) // .withUnlocked(vault.isUnlocked()) // - .withSavedPassword(vault.getPassword()) // + .withSavedPassword(vault.getPassword(), vault.getPasswordCryptoMode()) // .withFormat(vault.getFormat()) // .withShorteningThreshold(vault.getShorteningThreshold()) // .withPosition(vault.getPosition()); @@ -75,6 +79,10 @@ public String getPassword() { return password; } + public CryptoMode getPasswordCryptoMode() { + return passwordCryptoMode; + } + public int getFormat() { return format; } @@ -120,6 +128,7 @@ public static class Builder { private CloudType cloudType; private boolean unlocked; private String password; + private CryptoMode passwordCryptoMode; private int format = -1; private int shorteningThreshold = -1; private int position = -1; @@ -183,8 +192,9 @@ public Builder withNamePathAndCloudFrom(CloudFolder vaultFolder) { return this; } - public Builder withSavedPassword(String password) { + public Builder withSavedPassword(String password, CryptoMode cryptoMode) { this.password = password; + this.passwordCryptoMode = cryptoMode; return this; } @@ -224,6 +234,12 @@ private void validate() { if (position == -1) { throw new IllegalStateException("position must be set"); } + if (password != null && passwordCryptoMode == null) { + throw new IllegalStateException("passwordCryptoMode must be set if password is set"); + } + if (passwordCryptoMode != null && password == null) { + throw new IllegalStateException("password must be set if passwordCryptoMode is set"); + } } } } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/ListCBCEncryptedPasswordVaults.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/ListCBCEncryptedPasswordVaults.java new file mode 100644 index 000000000..379e86ffd --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/ListCBCEncryptedPasswordVaults.java @@ -0,0 +1,29 @@ +package org.cryptomator.domain.usecases.vault; + +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.VaultRepository; +import org.cryptomator.generator.UseCase; +import org.cryptomator.util.crypto.CryptoMode; + +import java.util.List; +import java.util.stream.Collectors; + +@UseCase +class ListCBCEncryptedPasswordVaults { + + private final VaultRepository vaultRepository; + + public ListCBCEncryptedPasswordVaults(VaultRepository vaultRepository) { + this.vaultRepository = vaultRepository; + } + + public List execute() throws BackendException { + return vaultRepository // + .vaults() // + .stream() // + .filter(vault -> vault.getPasswordCryptoMode() != null && vault.getPasswordCryptoMode().equals(CryptoMode.CBC)) // + .collect(Collectors.toUnmodifiableList()); + } + +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java index 28283f9c8..7fc81cf1a 100644 --- a/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswords.java @@ -1,41 +1,31 @@ package org.cryptomator.domain.usecases.vault; -import android.content.Context; - import org.cryptomator.domain.Vault; import org.cryptomator.domain.exception.BackendException; import org.cryptomator.domain.repository.VaultRepository; +import org.cryptomator.generator.Parameter; import org.cryptomator.generator.UseCase; -import org.cryptomator.util.SharedPreferencesHandler; -import org.cryptomator.util.crypto.BiometricAuthCryptor; -import org.cryptomator.util.crypto.CryptoMode; + +import java.util.List; import static org.cryptomator.domain.Vault.aCopyOf; @UseCase class RemoveStoredVaultPasswords { + private final List vaults; private final VaultRepository vaultRepository; - private final SharedPreferencesHandler sharedPreferencesHandler; - private final Context context; - public RemoveStoredVaultPasswords(VaultRepository vaultRepository, // - Context context, // - SharedPreferencesHandler sharedPreferencesHandler) { + public RemoveStoredVaultPasswords(@Parameter List vaults, VaultRepository vaultRepository) { + this.vaults = vaults; this.vaultRepository = vaultRepository; - this.context = context; - this.sharedPreferencesHandler = sharedPreferencesHandler; } public void execute() throws BackendException { - BiometricAuthCryptor.recreateKey(context, CryptoMode.GCM); - - sharedPreferencesHandler.changeUseBiometricAuthentication(false); - - for (Vault vault : vaultRepository.vaults()) { + for (Vault vault : vaults) { if (vault.getPassword() != null) { vault = aCopyOf(vault) // - .withSavedPassword(null) // + .withSavedPassword(null, null) // .build(); vaultRepository.store(vault); } diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswordsAndDisableBiometricAuth.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswordsAndDisableBiometricAuth.java new file mode 100644 index 000000000..075f3076e --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/RemoveStoredVaultPasswordsAndDisableBiometricAuth.java @@ -0,0 +1,44 @@ +package org.cryptomator.domain.usecases.vault; + +import android.content.Context; + +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.VaultRepository; +import org.cryptomator.generator.UseCase; +import org.cryptomator.util.SharedPreferencesHandler; +import org.cryptomator.util.crypto.BiometricAuthCryptor; +import org.cryptomator.util.crypto.CryptoMode; + +import static org.cryptomator.domain.Vault.aCopyOf; + +@UseCase +class RemoveStoredVaultPasswordsAndDisableBiometricAuth { + + private final VaultRepository vaultRepository; + private final SharedPreferencesHandler sharedPreferencesHandler; + private final Context context; + + public RemoveStoredVaultPasswordsAndDisableBiometricAuth(VaultRepository vaultRepository, // + Context context, // + SharedPreferencesHandler sharedPreferencesHandler) { + this.vaultRepository = vaultRepository; + this.context = context; + this.sharedPreferencesHandler = sharedPreferencesHandler; + } + + public void execute() throws BackendException { + BiometricAuthCryptor.recreateKey(context, CryptoMode.GCM); + + sharedPreferencesHandler.changeUseBiometricAuthentication(false); + + for (Vault vault : vaultRepository.vaults()) { + if (vault.getPassword() != null) { + vault = aCopyOf(vault) // + .withSavedPassword(null, null) // + .build(); + vaultRepository.store(vault); + } + } + } +} diff --git a/domain/src/main/java/org/cryptomator/domain/usecases/vault/SaveVaults.java b/domain/src/main/java/org/cryptomator/domain/usecases/vault/SaveVaults.java new file mode 100644 index 000000000..c3d6f21ac --- /dev/null +++ b/domain/src/main/java/org/cryptomator/domain/usecases/vault/SaveVaults.java @@ -0,0 +1,31 @@ +package org.cryptomator.domain.usecases.vault; + +import org.cryptomator.domain.Vault; +import org.cryptomator.domain.exception.BackendException; +import org.cryptomator.domain.repository.VaultRepository; +import org.cryptomator.generator.Parameter; +import org.cryptomator.generator.UseCase; + +import java.util.ArrayList; +import java.util.List; + +@UseCase +class SaveVaults { + + private final VaultRepository vaultRepository; + private final List vaults; + + public SaveVaults(VaultRepository vaultRepository, @Parameter List vaults) { + this.vaultRepository = vaultRepository; + this.vaults = vaults; + } + + public List execute() throws BackendException { + List storedVaults = new ArrayList<>(); + for (Vault vault : vaults) { + storedVaults.add(vaultRepository.store(vault)); + } + return storedVaults; + } + +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt index 177c1a52e..0cdbcd460 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/model/VaultModel.kt @@ -1,6 +1,7 @@ package org.cryptomator.presentation.model import org.cryptomator.domain.Vault +import org.cryptomator.util.crypto.CryptoMode import java.io.Serializable class VaultModel(private val vault: Vault) : Serializable { @@ -28,6 +29,8 @@ class VaultModel(private val vault: Vault) : Serializable { get() = CloudTypeModel.valueOf(vault.cloudType) val password: String? get() = vault.password + val passwordCryptoMode: CryptoMode? + get() = vault.passwordCryptoMode override fun equals(other: Any?): Boolean { return vault == (other as VaultModel).toVault() diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt index 51f3403ec..12fa18647 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/BiometricAuthSettingsPresenter.kt @@ -16,6 +16,7 @@ import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CryptoMode import javax.inject.Inject import timber.log.Timber @@ -77,7 +78,7 @@ class BiometricAuthSettingsPresenter @Inject constructor( // fun vaultUnlockedBiometricAuthPres(result: ActivityResult, vaultModel: VaultModel) { val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud val password = result.intent().getStringExtra(UnlockVaultPresenter.PASSWORD) - val vault = Vault.aCopyOf(vaultModel.toVault()).withCloud(cloud).withSavedPassword(password).build() + val vault = Vault.aCopyOf(vaultModel.toVault()).withCloud(cloud).withSavedPassword(password, CryptoMode.NONE).build() requestActivityResult( // ActivityResultCallbacks.encryptVaultPassword(vaultModel), // Intents.unlockVaultIntent().withVaultModel(VaultModel(vault)).withVaultAction(UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD) // @@ -87,7 +88,7 @@ class BiometricAuthSettingsPresenter @Inject constructor( // @Callback fun encryptVaultPassword(result: ActivityResult, vaultModel: VaultModel) { val tmpVault = result.intent().getSerializableExtra(SINGLE_RESULT) as VaultModel - val vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(tmpVault.password).build() + val vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(tmpVault.password, tmpVault.passwordCryptoMode).build() saveVault(vault) } @@ -122,7 +123,7 @@ class BiometricAuthSettingsPresenter @Inject constructor( // private fun removePasswordAndSave(vault: Vault) { val vaultWithRemovedPassword = Vault // .aCopyOf(vault) // - .withSavedPassword(null) // + .withSavedPassword(null, null) // .build() saveVault(vaultWithRemovedPassword) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index 14180e85b..3d57504c4 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -15,7 +15,7 @@ import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.GetUnverifiedVaultConfigUseCase import org.cryptomator.domain.usecases.vault.LockVaultUseCase import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase -import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase +import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsAndDisableBiometricAuthUseCase import org.cryptomator.domain.usecases.vault.SaveVaultUseCase import org.cryptomator.domain.usecases.vault.UnlockToken import org.cryptomator.domain.usecases.vault.UnlockVaultUsingMasterkeyUseCase @@ -34,6 +34,7 @@ import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CryptoMode import java.io.Serializable import javax.inject.Inject import timber.log.Timber @@ -46,7 +47,7 @@ class UnlockVaultPresenter @Inject constructor( private val lockVaultUseCase: LockVaultUseCase, private val unlockVaultUsingMasterkeyUseCase: UnlockVaultUsingMasterkeyUseCase, private val prepareUnlockUseCase: PrepareUnlockUseCase, - private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, + private val removeStoredVaultPasswordsAndDisableBiometricAuthUseCase: RemoveStoredVaultPasswordsAndDisableBiometricAuthUseCase, private val saveVaultUseCase: SaveVaultUseCase, private val authenticationExceptionHandler: AuthenticationExceptionHandler, private val sharedPreferencesHandler: SharedPreferencesHandler, @@ -304,7 +305,7 @@ class UnlockVaultPresenter @Inject constructor( } fun onBiometricKeyInvalidated() { - removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler() { + removeStoredVaultPasswordsAndDisableBiometricAuthUseCase.run(object : DefaultResultHandler() { override fun onSuccess(void: Void?) { view?.showBiometricAuthKeyInvalidatedDialog() } @@ -353,7 +354,7 @@ class UnlockVaultPresenter @Inject constructor( view?.getEncryptedPasswordWithBiometricAuthentication( VaultModel( // Vault.aCopyOf(vaultModel.toVault()) // - .withSavedPassword(newPassword) // + .withSavedPassword(newPassword, CryptoMode.GCM) // .build() ) ) @@ -458,7 +459,7 @@ class UnlockVaultPresenter @Inject constructor( lockVaultUseCase, // unlockVaultUsingMasterkeyUseCase, // prepareUnlockUseCase, // - removeStoredVaultPasswordsUseCase, // + removeStoredVaultPasswordsAndDisableBiometricAuthUseCase, // saveVaultUseCase ) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt index ddab5bd29..6750b1241 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/VaultListPresenter.kt @@ -28,10 +28,13 @@ import org.cryptomator.domain.usecases.UpdateCheck import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase import org.cryptomator.domain.usecases.vault.GetVaultListUseCase +import org.cryptomator.domain.usecases.vault.ListCBCEncryptedPasswordVaultsUseCase import org.cryptomator.domain.usecases.vault.LockVaultUseCase import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase +import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase import org.cryptomator.domain.usecases.vault.RenameVaultUseCase import org.cryptomator.domain.usecases.vault.SaveVaultUseCase +import org.cryptomator.domain.usecases.vault.SaveVaultsUseCase import org.cryptomator.domain.usecases.vault.UpdateVaultParameterIfChangedRemotelyUseCase import org.cryptomator.generator.Callback import org.cryptomator.presentation.BuildConfig @@ -49,6 +52,7 @@ import org.cryptomator.presentation.ui.activity.LicenseCheckActivity import org.cryptomator.presentation.ui.activity.view.VaultListView import org.cryptomator.presentation.ui.dialog.AppIsObscuredInfoDialog import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog +import org.cryptomator.presentation.ui.dialog.CBCPasswordVaultsMigrationDialog import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog import org.cryptomator.presentation.ui.dialog.UpdateAppDialog @@ -61,6 +65,7 @@ import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow import org.cryptomator.presentation.workflow.PermissionsResult import org.cryptomator.presentation.workflow.Workflow import org.cryptomator.util.SharedPreferencesHandler +import org.cryptomator.util.crypto.CryptoMode import javax.inject.Inject import timber.log.Timber @@ -80,6 +85,9 @@ class VaultListPresenter @Inject constructor( // private val updateCheckUseCase: DoUpdateCheckUseCase, // private val updateUseCase: DoUpdateUseCase, // private val updateVaultParameterIfChangedRemotelyUseCase: UpdateVaultParameterIfChangedRemotelyUseCase, // + private val listCBCEncryptedPasswordVaultsUseCase: ListCBCEncryptedPasswordVaultsUseCase, // + private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // + private val saveVaultsUseCase: SaveVaultsUseCase, // private val networkConnectionCheck: NetworkConnectionCheck, // private val fileUtil: FileUtil, // private val authenticationExceptionHandler: AuthenticationExceptionHandler, // @@ -232,8 +240,66 @@ class VaultListPresenter @Inject constructor( // if (!result.granted()) { Timber.tag("VaultListPresenter").e("Notification permission not granted, notifications will not show") } + checkCBCEncryptedVaults() } + private fun checkCBCEncryptedVaults() { + listCBCEncryptedPasswordVaultsUseCase + .run(object : DefaultResultHandler>() { + override fun onSuccess(vaults: List) { + if (vaults.isNotEmpty()) { + view?.showDialog(CBCPasswordVaultsMigrationDialog.newInstance(vaults)) + } + } + }) + } + + fun cBCPasswordVaultsMigrationClicked(cbcVaults: List) { + val vaultModels = cbcVaults.mapTo(ArrayList()) { VaultModel(it) } + view?.migrateCBCEncryptedPasswordVaults(vaultModels) + } + + fun cBCPasswordVaultsMigrationRejected(cbcVaults: List) { + removeStoredVaultPasswordsUseCase + .withVaults(cbcVaults) + .run(object : DefaultResultHandler() { + override fun onSuccess(ignore: Void?) { + loadVaultList() + } + }) + } + + fun biometricAuthenticationMigrationFinished(vaultModels: List) { + val vaults = vaultModels.map { vaultModel -> vaultModel.toVault() } + saveVaultsUseCase // + .withVaults(vaults) // + .run(object : NoOpResultHandler>() { + override fun onSuccess(migratedVaults: List) { + loadVaultList() + } + + override fun onError(e: Throwable) { + showError(e) + } + }) + } + + + fun biometricKeyInvalidated(cbcVaults: List) { + val vaults = cbcVaults.map { vaultModel -> vaultModel.toVault() } + removeStoredVaultPasswordsUseCase + .withVaults(vaults) + .run(object : DefaultResultHandler() { + override fun onSuccess(ignore: Void?) { + loadVaultList() + } + }) + } + + fun biometricAuthenticationFailed(cbcVaults: List) { + val vaults = cbcVaults.map { vaultModel -> vaultModel.toVault() } + view?.showDialog(CBCPasswordVaultsMigrationDialog.newInstance(vaults)) + } fun loadVaultList() { view?.hideVaultCreationHint() @@ -351,21 +417,32 @@ class VaultListPresenter @Inject constructor( // } private fun startVaultAction(vault: VaultModel, vaultAction: VaultAction) { - this.vaultAction = vaultAction - val cloud = vault.toVault().cloud - if (cloud != null) { - onCloudOfVaultAuthenticated(vault.toVault()) + if (vault.passwordCryptoMode?.equals(CryptoMode.CBC) == true) { + listCBCEncryptedPasswordVaultsUseCase + .run(object : DefaultResultHandler>() { + override fun onSuccess(vaults: List) { + if (vaults.isNotEmpty()) { + view?.showDialog(CBCPasswordVaultsMigrationDialog.newInstance(vaults)) + } + } + }) } else { - if (vault.isLocked) { - onVaultWithoutCloudClickedAndLocked(vault) + this.vaultAction = vaultAction + val cloud = vault.toVault().cloud + if (cloud != null) { + onCloudOfVaultAuthenticated(vault.toVault()) } else { - lockVaultUseCase // - .withVault(vault.toVault()) // - .run(object : DefaultResultHandler() { - override fun onSuccess(vault: Vault) { - onVaultWithoutCloudClickedAndLocked(VaultModel(vault)) - } - }) + if (vault.isLocked) { + onVaultWithoutCloudClickedAndLocked(vault) + } else { + lockVaultUseCase // + .withVault(vault.toVault()) // + .run(object : DefaultResultHandler() { + override fun onSuccess(vault: Vault) { + onVaultWithoutCloudClickedAndLocked(VaultModel(vault)) + } + }) + } } } } @@ -570,6 +647,9 @@ class VaultListPresenter @Inject constructor( // licenseCheckUseCase, // updateCheckUseCase, // updateUseCase, // + listCBCEncryptedPasswordVaultsUseCase, // + removeStoredVaultPasswordsUseCase, // + saveVaultsUseCase, // updateVaultParameterIfChangedRemotelyUseCase ) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt index 1271df05d..8014d7c87 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt @@ -23,8 +23,8 @@ import javax.inject.Inject @Activity class UnlockVaultActivity : BaseActivity(ActivityUnlockVaultBinding::inflate), // UnlockVaultView, // - BiometricAuthentication.Callback, - ChangePasswordDialog.Callback, + BiometricAuthentication.Callback, // + ChangePasswordDialog.Callback, // VaultNotFoundDialog.Callback { @Inject @@ -84,7 +84,7 @@ class UnlockVaultActivity : BaseActivity(ActivityUnl } override fun onBiometricAuthenticationFailed(vault: VaultModel) { - val vaultWithoutPassword = Vault.aCopyOf(vault.toVault()).withSavedPassword(null).build() + val vaultWithoutPassword = Vault.aCopyOf(vault.toVault()).withSavedPassword(null, null).build() when (unlockVaultIntent.vaultAction()) { UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> presenter.saveVaultAfterChangePasswordButFailedBiometricAuth(vaultWithoutPassword) else -> { diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt index fd124b2a6..e6fba267a 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/VaultListActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.View import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.Fragment +import org.cryptomator.domain.Vault import org.cryptomator.generator.Activity import org.cryptomator.generator.InjectIntent import org.cryptomator.presentation.CryptomatorApp @@ -25,12 +26,14 @@ import org.cryptomator.presentation.ui.bottomsheet.SettingsVaultBottomSheet import org.cryptomator.presentation.ui.callback.VaultListCallback import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog import org.cryptomator.presentation.ui.dialog.BetaConfirmationDialog +import org.cryptomator.presentation.ui.dialog.CBCPasswordVaultsMigrationDialog import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog import org.cryptomator.presentation.ui.dialog.UpdateAppDialog import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog import org.cryptomator.presentation.ui.dialog.VaultRenameDialog import org.cryptomator.presentation.ui.fragment.VaultListFragment import org.cryptomator.presentation.ui.layout.ObscuredAwareCoordinatorLayout.Listener +import org.cryptomator.presentation.util.BiometricAuthenticationMigration import javax.inject.Inject @Activity @@ -40,7 +43,9 @@ class VaultListActivity : BaseActivity(Activi AskForLockScreenDialog.Callback, // UpdateAppAvailableDialog.Callback, // UpdateAppDialog.Callback, // - BetaConfirmationDialog.Callback { + BetaConfirmationDialog.Callback, // + CBCPasswordVaultsMigrationDialog.Callback, // + BiometricAuthenticationMigration.Callback { @Inject lateinit var vaultListPresenter: VaultListPresenter @@ -48,6 +53,8 @@ class VaultListActivity : BaseActivity(Activi @InjectIntent lateinit var vaultListIntent: VaultListIntent + private lateinit var biometricAuthenticationMigration: BiometricAuthenticationMigration + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) @@ -125,6 +132,11 @@ class VaultListActivity : BaseActivity(Activi vaultListFragment().vaultMoved(vaults) } + override fun migrateCBCEncryptedPasswordVaults(vaults: List) { + biometricAuthenticationMigration = BiometricAuthenticationMigration(this, context(), sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication()) + biometricAuthenticationMigration.migrateVaultsPassword(vaultListFragment(), vaults) + } + override fun showVaultSettingsDialog(vaultModel: VaultModel) { val vaultSettingDialog = // SettingsVaultBottomSheet.newInstance(vaultModel) @@ -221,4 +233,25 @@ class VaultListActivity : BaseActivity(Activi override fun onAskForBetaConfirmationFinished() { sharedPreferencesHandler.setBetaScreenDialogAlreadyShown(true) } + + override fun onCBCPasswordVaultsMigrationClicked(cbcVaults: List) { + vaultListPresenter.cBCPasswordVaultsMigrationClicked(cbcVaults) + } + + override fun onCBCPasswordVaultsMigrationRejected(cbcVaults: List) { + vaultListPresenter.cBCPasswordVaultsMigrationRejected(cbcVaults) + } + + override fun onBiometricAuthenticationMigrationFinished(vaults: List) { + vaultListPresenter.biometricAuthenticationMigrationFinished(vaults) + } + + override fun onBiometricAuthenticationFailed(vaults: List) { + vaultListPresenter.biometricAuthenticationFailed(vaults) + } + + override fun onBiometricKeyInvalidated(vaults: List) { + vaultListPresenter.biometricKeyInvalidated(vaults) + } + } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt index e9950de5f..d4abae251 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/VaultListView.kt @@ -18,5 +18,6 @@ interface VaultListView : View { fun isVaultLocked(vaultModel: VaultModel): Boolean fun rowMoved(fromPosition: Int, toPosition: Int) fun vaultMoved(vaults: List) + fun migrateCBCEncryptedPasswordVaults(vaults: List) } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt new file mode 100644 index 000000000..3f3d8078e --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt @@ -0,0 +1,46 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import org.cryptomator.domain.Vault +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogCbcPasswordVaultsMigrationBinding + +@Dialog +class CBCPasswordVaultsMigrationDialog : BaseDialog(DialogCbcPasswordVaultsMigrationBinding::inflate) { + + interface Callback { + + fun onCBCPasswordVaultsMigrationClicked(cbcVaults: List) + fun onCBCPasswordVaultsMigrationRejected(cbcVaults: List) + + } + + override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + val cbcVaults = requireArguments().getSerializable(VAULTS_ARG) as ArrayList + builder // + .setTitle(R.string.dialog_cbc_password_vaults_migration_title) // + .setPositiveButton(getString(R.string.dialog_cbc_password_vaults_migration_action)) { _: DialogInterface, _: Int -> callback?.onCBCPasswordVaultsMigrationClicked(cbcVaults) } // + .setNegativeButton(getString(R.string.dialog_cbc_password_vaults_migration_cancel)) { _: DialogInterface, _: Int -> callback?.onCBCPasswordVaultsMigrationRejected(cbcVaults) } + return builder.create() + } + + public override fun setupView() { + // empty + } + + companion object { + + private const val VAULTS_ARG = "vaults" + fun newInstance(cbcVaults: List): DialogFragment { + val dialog = CBCPasswordVaultsMigrationDialog() + val args = Bundle() + args.putSerializable(VAULTS_ARG, ArrayList(cbcVaults)) + dialog.arguments = args + return dialog + } + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt index 341d533f5..7b63b78ef 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt @@ -9,7 +9,6 @@ import org.cryptomator.domain.Vault import org.cryptomator.presentation.R import org.cryptomator.presentation.model.VaultModel import org.cryptomator.util.crypto.BiometricAuthCryptor -import org.cryptomator.util.crypto.CryptoMode import org.cryptomator.util.crypto.UnrecoverableStorageKeyException import java.util.concurrent.Executor import javax.crypto.BadPaddingException @@ -82,7 +81,7 @@ class BiometricAuthentication(val callback: Callback, val context: Context, val val vaultModelPasswordAware = VaultModel( Vault // .aCopyOf(vaultModel.toVault()) // - .withSavedPassword(transformedPassword) // + .withSavedPassword(transformedPassword, org.cryptomator.util.crypto.CryptoMode.GCM) // .build() ) diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt new file mode 100644 index 000000000..1ddea3d4b --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt @@ -0,0 +1,161 @@ +package org.cryptomator.presentation.util + +import android.content.Context +import android.security.keystore.KeyPermanentlyInvalidatedException +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import org.cryptomator.domain.Vault +import org.cryptomator.presentation.R +import org.cryptomator.presentation.model.VaultModel +import org.cryptomator.util.crypto.BiometricAuthCryptor +import org.cryptomator.util.crypto.CryptoMode +import org.cryptomator.util.crypto.UnrecoverableStorageKeyException +import javax.crypto.BadPaddingException +import timber.log.Timber + +class BiometricAuthenticationMigration(val callback: Callback, val context: Context, private val useConfirmationInFaceUnlockAuth: Boolean) { + + interface Callback { + + fun onBiometricAuthenticationMigrationFinished(vaults: List) + fun onBiometricAuthenticationFailed(vaults: List) + fun onBiometricKeyInvalidated(vaults: List) + + } + + companion object { + + private lateinit var promptInfo: BiometricPrompt.PromptInfo + + } + + fun migrateVaultsPassword(fragment: Fragment, vaultModels: List) { + val decryptedVaults = mutableListOf() + val vaultQueue = ArrayDeque(vaultModels) + + promptInfo = BiometricPrompt.PromptInfo.Builder() // + .setTitle(context.getString(R.string.dialog_biometric_auth_title)) // + .setSubtitle(context.getString(R.string.dialog_biometric_auth_message)) // + .setConfirmationRequired(useConfirmationInFaceUnlockAuth) // + .setNegativeButtonText(context.getString(R.string.dialog_biometric_auth_use_password)) // + .build() + + // Start processing the queue + processNextVault(fragment, vaultQueue, decryptedVaults) + } + + private fun processNextVault(fragment: Fragment, vaultQueue: ArrayDeque, decryptedVaults: MutableList) { + if (vaultQueue.isEmpty()) { + encryptUsingGcm(fragment, decryptedVaults) + } else { + val currentVault = vaultQueue.removeFirst() // Get the next vault to process + decryptVaultPassword(fragment, currentVault, decryptedVaults, vaultQueue) + } + } + + private fun decryptVaultPassword(fragment: Fragment, vaultModel: VaultModel, decryptedVaults: MutableList, vaultQueue: ArrayDeque) { + Timber.tag("BiometricAuthenticationMigration").d("Show decrypt biometric auth prompt") + try { + val biometricAuthCryptorCBC = BiometricAuthCryptor.getInstance(context, CryptoMode.CBC) + val authCallback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Timber.tag("BiometricAuthenticationMigration").d("Authentication finished successfully") + val cipher = result.cryptoObject?.cipher + try { + val decryptedPassword = biometricAuthCryptorCBC.decrypt(cipher, vaultModel.password) + val decryptedVaultModel = VaultModel( + Vault.aCopyOf(vaultModel.toVault()) + .withSavedPassword(decryptedPassword, CryptoMode.NONE) + .build() + ) + // Add the decrypted vault to the list + decryptedVaults.add(decryptedVaultModel) + // Process the next vault + processNextVault(fragment, vaultQueue, decryptedVaults) + } catch (e: BadPaddingException) { + Timber.tag("BiometricAuthenticationMigration").i( + e, + "Recover from BadPaddingException which can be thrown on some devices if the key in the keystore is invalidated e.g. due to a fingerprint added because of an upstream error in Android, see #400 for more info" + ) + callback.onBiometricKeyInvalidated(decryptedVaults) + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Timber.tag("BiometricAuthenticationMigration").e(String.format("Authentication error: %s errorCode=%d", errString, errorCode)) + callback.onBiometricAuthenticationFailed(decryptedVaults) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Timber.tag("BiometricAuthenticationMigration").e("Authentication failed") + } + } + val biometricPrompt = BiometricPrompt(fragment, ContextCompat.getMainExecutor(context), authCallback) + try { + val cryptoCipher = biometricAuthCryptorCBC.getDecryptCipher(vaultModel.password) + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cryptoCipher)) + } catch (e: KeyPermanentlyInvalidatedException) { + callback.onBiometricKeyInvalidated(decryptedVaults) + } + } catch (e: UnrecoverableStorageKeyException) { + callback.onBiometricKeyInvalidated(listOf(vaultModel)) + } + } + + private fun encryptUsingGcm(fragment: Fragment, vaultModels: List) { + Timber.tag("BiometricAuthenticationMigration").d("Show encrypt biometric auth prompt") + val biometricAuthCryptorGCM: BiometricAuthCryptor + try { + biometricAuthCryptorGCM = BiometricAuthCryptor.getInstance(context, CryptoMode.GCM) + } catch (e: UnrecoverableStorageKeyException) { + return callback.onBiometricKeyInvalidated(vaultModels) + } + val authCallback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Timber.tag("BiometricAuthenticationMigration").d("Authentication finished successfully") + val cipher = result.cryptoObject?.cipher + try { + val gcmEncryptedVaults = vaultModels.map { vaultModel -> + val encryptedPassword = biometricAuthCryptorGCM.encrypt(cipher, vaultModel.password) + VaultModel( + Vault // + .aCopyOf(vaultModel.toVault()) // + .withSavedPassword(encryptedPassword, CryptoMode.GCM) // + .build() + ) + } + callback.onBiometricAuthenticationMigrationFinished(gcmEncryptedVaults) + } catch (e: BadPaddingException) { + Timber.tag("BiometricAuthenticationMigration").i( + e, + "Recover from BadPaddingException which can be thrown on some devices if the key in the keystore is invalidated e.g. due to a fingerprint added because of an upstream error in Android, see #400 for more info" + ) + callback.onBiometricKeyInvalidated(vaultModels) + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Timber.tag("BiometricAuthenticationMigration").e(String.format("Authentication error: %s errorCode=%d", errString, errorCode)) + callback.onBiometricAuthenticationFailed(vaultModels) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Timber.tag("BiometricAuthenticationMigration").e("Authentication failed") + } + } + val biometricPrompt = BiometricPrompt(fragment, ContextCompat.getMainExecutor(context), authCallback) + try { + val cryptoCipher = biometricAuthCryptorGCM.encryptCipher + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cryptoCipher)) + } catch (e: KeyPermanentlyInvalidatedException) { + callback.onBiometricKeyInvalidated(vaultModels) + } + } +} diff --git a/presentation/src/main/res/layout/dialog_cbc_password_vaults_migration.xml b/presentation/src/main/res/layout/dialog_cbc_password_vaults_migration.xml new file mode 100644 index 000000000..266f38951 --- /dev/null +++ b/presentation/src/main/res/layout/dialog_cbc_password_vaults_migration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 359adefd0..4bc761926 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -519,6 +519,11 @@ Credentials of \'%1$s\' updated If you intended to add a new pCloud account, tap on this url www.pcloud.com, log out from the current account and tap again on the \'+\' in this app to create a new cloud connection. + Vault password migration required + Due to security enhancements, you will be prompted for your biometric authentication multiple times to re-encrypt your vault passwords. This is necessary to continue to protect your vault passwords with the latest technology. If you do not wish this to happen, the stored passwords will be removed. + Migrate + @string/dialog_button_cancel + Cryptomator needs storage access to use local vaults Cryptomator needs storage access to use auto photo upload Cryptomator needs notification permissions to display vault status for example diff --git a/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java b/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java index a8bdc0199..fdd20b7ce 100644 --- a/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java +++ b/presentation/src/test/java/org/cryptomator/presentation/presenter/VaultListPresenterTest.java @@ -15,10 +15,13 @@ import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase; import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase; import org.cryptomator.domain.usecases.vault.GetVaultListUseCase; +import org.cryptomator.domain.usecases.vault.ListCBCEncryptedPasswordVaultsUseCase; import org.cryptomator.domain.usecases.vault.LockVaultUseCase; import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase; +import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase; import org.cryptomator.domain.usecases.vault.RenameVaultUseCase; import org.cryptomator.domain.usecases.vault.SaveVaultUseCase; +import org.cryptomator.domain.usecases.vault.SaveVaultsUseCase; import org.cryptomator.domain.usecases.vault.UnlockToken; import org.cryptomator.domain.usecases.vault.UpdateVaultParameterIfChangedRemotelyUseCase; import org.cryptomator.presentation.exception.ExceptionHandlers; @@ -105,6 +108,9 @@ public class VaultListPresenterTest { private DoUpdateCheckUseCase updateCheckUseCase = Mockito.mock(DoUpdateCheckUseCase.class); private DoUpdateUseCase updateUseCase = Mockito.mock(DoUpdateUseCase.class); private UpdateVaultParameterIfChangedRemotelyUseCase updateVaultParameterIfChangedRemotelyUseCase = Mockito.mock(UpdateVaultParameterIfChangedRemotelyUseCase.class); + private ListCBCEncryptedPasswordVaultsUseCase listCBCEncryptedPasswordVaultsUseCase = Mockito.mock(ListCBCEncryptedPasswordVaultsUseCase.class); + private RemoveStoredVaultPasswordsUseCase removeStoredVaultPasswordsUseCase = Mockito.mock(RemoveStoredVaultPasswordsUseCase.class); + private SaveVaultsUseCase saveVaultsUseCase = Mockito.mock(SaveVaultsUseCase.class); private NetworkConnectionCheck networkConnectionCheck = Mockito.mock(NetworkConnectionCheck.class); private FileUtil fileUtil = Mockito.mock(FileUtil.class); private AuthenticationExceptionHandler authenticationExceptionHandler = Mockito.mock(AuthenticationExceptionHandler.class); @@ -128,6 +134,9 @@ public void setup() { updateCheckUseCase, // updateUseCase, // updateVaultParameterIfChangedRemotelyUseCase, // + listCBCEncryptedPasswordVaultsUseCase, // + removeStoredVaultPasswordsUseCase, // + saveVaultsUseCase, // networkConnectionCheck, // fileUtil, // authenticationExceptionHandler, // diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java index 0e57bef20..02848382c 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoMode.java @@ -1,5 +1,5 @@ package org.cryptomator.util.crypto; public enum CryptoMode { - CBC, GCM; + CBC, GCM, NONE; } From 89b8aa67613818c0be1db353db291e2210587334 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Mon, 12 Aug 2024 17:32:45 +0200 Subject: [PATCH 4/7] Add workaround to show multiple biometric auth promots from within the same Activity --- .../util/BiometricAuthenticationMigration.kt | 177 ++++++++---------- presentation/src/main/res/values/strings.xml | 8 +- 2 files changed, 85 insertions(+), 100 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt index 1ddea3d4b..4913b42dd 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt @@ -1,8 +1,11 @@ package org.cryptomator.presentation.util +import android.annotation.SuppressLint import android.content.Context import android.security.keystore.KeyPermanentlyInvalidatedException +import androidx.biometric.BiometricFragment import androidx.biometric.BiometricPrompt +import androidx.biometric.FingerprintDialogFragment import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import org.cryptomator.domain.Vault @@ -14,148 +17,126 @@ import org.cryptomator.util.crypto.UnrecoverableStorageKeyException import javax.crypto.BadPaddingException import timber.log.Timber -class BiometricAuthenticationMigration(val callback: Callback, val context: Context, private val useConfirmationInFaceUnlockAuth: Boolean) { +class BiometricAuthenticationMigration( + private val callback: Callback, private val context: Context, private val useConfirmationInFaceUnlockAuth: Boolean +) { interface Callback { fun onBiometricAuthenticationMigrationFinished(vaults: List) fun onBiometricAuthenticationFailed(vaults: List) fun onBiometricKeyInvalidated(vaults: List) - } - companion object { - - private lateinit var promptInfo: BiometricPrompt.PromptInfo - - } + private lateinit var promptInfo: BiometricPrompt.PromptInfo fun migrateVaultsPassword(fragment: Fragment, vaultModels: List) { val decryptedVaults = mutableListOf() + val reEncryptedVaults = mutableListOf() val vaultQueue = ArrayDeque(vaultModels) promptInfo = BiometricPrompt.PromptInfo.Builder() // - .setTitle(context.getString(R.string.dialog_biometric_auth_title)) // - .setSubtitle(context.getString(R.string.dialog_biometric_auth_message)) // + .setTitle(context.getString(R.string.dialog_biometric_migration_auth_title)) // + .setSubtitle(context.getString(R.string.dialog_biometric_migration_auth_message)) // .setConfirmationRequired(useConfirmationInFaceUnlockAuth) // - .setNegativeButtonText(context.getString(R.string.dialog_biometric_auth_use_password)) // + .setNegativeButtonText(context.getString(R.string.dialog_biometric_migration_auth_use_password)) // .build() - // Start processing the queue - processNextVault(fragment, vaultQueue, decryptedVaults) + processNextVault(fragment, vaultQueue, decryptedVaults, reEncryptedVaults, vaultModels) } - private fun processNextVault(fragment: Fragment, vaultQueue: ArrayDeque, decryptedVaults: MutableList) { - if (vaultQueue.isEmpty()) { - encryptUsingGcm(fragment, decryptedVaults) - } else { - val currentVault = vaultQueue.removeFirst() // Get the next vault to process - decryptVaultPassword(fragment, currentVault, decryptedVaults, vaultQueue) + private fun processNextVault( + fragment: Fragment, vaultQueue: ArrayDeque, decryptedVaults: MutableList, reEncryptedVaults: MutableList, allVaults: List + ) { + removeBiometricFragmentFromStack(fragment) + when { + vaultQueue.isNotEmpty() -> decryptUsingCbc(fragment, vaultQueue.removeFirst(), decryptedVaults, vaultQueue, reEncryptedVaults, allVaults) + decryptedVaults.isNotEmpty() -> encryptUsingGcm(fragment, decryptedVaults.removeFirst(), vaultQueue, decryptedVaults, reEncryptedVaults, allVaults) + else -> callback.onBiometricAuthenticationMigrationFinished(reEncryptedVaults) } } - private fun decryptVaultPassword(fragment: Fragment, vaultModel: VaultModel, decryptedVaults: MutableList, vaultQueue: ArrayDeque) { - Timber.tag("BiometricAuthenticationMigration").d("Show decrypt biometric auth prompt") + @SuppressLint("RestrictedApi") + private fun removeBiometricFragmentFromStack(fragment: Fragment) { + val fragmentManager = fragment.childFragmentManager + fragmentManager.fragments.filter { it is BiometricFragment || it is FingerprintDialogFragment }.forEach { fragmentManager.beginTransaction().remove(it).commitNow() } + } + + private fun decryptUsingCbc( + fragment: Fragment, vaultModel: VaultModel, decryptedVaults: MutableList, vaultQueue: ArrayDeque, reEncryptedVaults: MutableList, allVaults: List + ) { + Timber.tag("BiometricAuthMigration").d("Prompt for decryption") + handleBiometricAuthentication(fragment = fragment, cryptoMode = CryptoMode.CBC, password = vaultModel.password!!, allVaults = allVaults, onSuccess = { decryptedPassword -> + decryptedVaults.add( + VaultModel( + vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(decryptedPassword, CryptoMode.NONE).build() + ) + ) + processNextVault(fragment, vaultQueue, decryptedVaults, reEncryptedVaults, allVaults) + }) + } + + private fun encryptUsingGcm( + fragment: Fragment, vaultModel: VaultModel, vaultQueue: ArrayDeque, decryptedVaults: MutableList, reEncryptedVaults: MutableList, allVaults: List + ) { + Timber.tag("BiometricAuthMigration").d("Prompt for encryption") + handleBiometricAuthentication(fragment = fragment, cryptoMode = CryptoMode.GCM, password = vaultModel.password!!, allVaults = allVaults, onSuccess = { encryptedPassword -> + reEncryptedVaults.add( + VaultModel( + vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(encryptedPassword, CryptoMode.GCM).build() + ) + ) + processNextVault(fragment, vaultQueue, decryptedVaults, reEncryptedVaults, allVaults) + }) + } + + private fun handleBiometricAuthentication( + fragment: Fragment, cryptoMode: CryptoMode, password: String, allVaults: List, onSuccess: (String) -> Unit + ) { try { - val biometricAuthCryptorCBC = BiometricAuthCryptor.getInstance(context, CryptoMode.CBC) + val biometricAuthCryptor = BiometricAuthCryptor.getInstance(context, cryptoMode) val authCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) - Timber.tag("BiometricAuthenticationMigration").d("Authentication finished successfully") + Timber.tag("BiometricAuthMigration").d("Authentication succeeded") val cipher = result.cryptoObject?.cipher try { - val decryptedPassword = biometricAuthCryptorCBC.decrypt(cipher, vaultModel.password) - val decryptedVaultModel = VaultModel( - Vault.aCopyOf(vaultModel.toVault()) - .withSavedPassword(decryptedPassword, CryptoMode.NONE) - .build() - ) - // Add the decrypted vault to the list - decryptedVaults.add(decryptedVaultModel) - // Process the next vault - processNextVault(fragment, vaultQueue, decryptedVaults) + val processedPassword = when (cryptoMode) { + CryptoMode.CBC -> biometricAuthCryptor.decrypt(cipher, password) + CryptoMode.GCM -> biometricAuthCryptor.encrypt(cipher, password) + CryptoMode.NONE -> throw IllegalStateException("CryptoMode.NONE is not allowed here") + } + onSuccess(processedPassword) } catch (e: BadPaddingException) { - Timber.tag("BiometricAuthenticationMigration").i( - e, - "Recover from BadPaddingException which can be thrown on some devices if the key in the keystore is invalidated e.g. due to a fingerprint added because of an upstream error in Android, see #400 for more info" - ) - callback.onBiometricKeyInvalidated(decryptedVaults) + Timber.e(e, "BadPaddingException - possibly due to an invalidated key") + callback.onBiometricKeyInvalidated(allVaults) } } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) - Timber.tag("BiometricAuthenticationMigration").e(String.format("Authentication error: %s errorCode=%d", errString, errorCode)) - callback.onBiometricAuthenticationFailed(decryptedVaults) + Timber.e("Authentication error: %s errorCode=%d", errString, errorCode) + callback.onBiometricAuthenticationFailed(allVaults) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() - Timber.tag("BiometricAuthenticationMigration").e("Authentication failed") + Timber.e("Authentication failed") } } val biometricPrompt = BiometricPrompt(fragment, ContextCompat.getMainExecutor(context), authCallback) - try { - val cryptoCipher = biometricAuthCryptorCBC.getDecryptCipher(vaultModel.password) - biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cryptoCipher)) - } catch (e: KeyPermanentlyInvalidatedException) { - callback.onBiometricKeyInvalidated(decryptedVaults) + val cryptoCipher = when (cryptoMode) { + CryptoMode.CBC -> biometricAuthCryptor.getDecryptCipher(password) + CryptoMode.GCM -> biometricAuthCryptor.encryptCipher + CryptoMode.NONE -> throw IllegalStateException("CryptoMode.NONE is not allowed here") } - } catch (e: UnrecoverableStorageKeyException) { - callback.onBiometricKeyInvalidated(listOf(vaultModel)) - } - } - - private fun encryptUsingGcm(fragment: Fragment, vaultModels: List) { - Timber.tag("BiometricAuthenticationMigration").d("Show encrypt biometric auth prompt") - val biometricAuthCryptorGCM: BiometricAuthCryptor - try { - biometricAuthCryptorGCM = BiometricAuthCryptor.getInstance(context, CryptoMode.GCM) - } catch (e: UnrecoverableStorageKeyException) { - return callback.onBiometricKeyInvalidated(vaultModels) - } - val authCallback = object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - Timber.tag("BiometricAuthenticationMigration").d("Authentication finished successfully") - val cipher = result.cryptoObject?.cipher - try { - val gcmEncryptedVaults = vaultModels.map { vaultModel -> - val encryptedPassword = biometricAuthCryptorGCM.encrypt(cipher, vaultModel.password) - VaultModel( - Vault // - .aCopyOf(vaultModel.toVault()) // - .withSavedPassword(encryptedPassword, CryptoMode.GCM) // - .build() - ) - } - callback.onBiometricAuthenticationMigrationFinished(gcmEncryptedVaults) - } catch (e: BadPaddingException) { - Timber.tag("BiometricAuthenticationMigration").i( - e, - "Recover from BadPaddingException which can be thrown on some devices if the key in the keystore is invalidated e.g. due to a fingerprint added because of an upstream error in Android, see #400 for more info" - ) - callback.onBiometricKeyInvalidated(vaultModels) - } - } - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - Timber.tag("BiometricAuthenticationMigration").e(String.format("Authentication error: %s errorCode=%d", errString, errorCode)) - callback.onBiometricAuthenticationFailed(vaultModels) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Timber.tag("BiometricAuthenticationMigration").e("Authentication failed") - } - } - val biometricPrompt = BiometricPrompt(fragment, ContextCompat.getMainExecutor(context), authCallback) - try { - val cryptoCipher = biometricAuthCryptorGCM.encryptCipher biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cryptoCipher)) } catch (e: KeyPermanentlyInvalidatedException) { - callback.onBiometricKeyInvalidated(vaultModels) + Timber.e("KeyPermanentlyInvalidatedException during $cryptoMode") + callback.onBiometricKeyInvalidated(allVaults) + } catch (e: UnrecoverableStorageKeyException) { + Timber.e("UnrecoverableStorageKeyException during $cryptoMode") + callback.onBiometricKeyInvalidated(allVaults) } } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 4bc761926..55bee9500 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -520,9 +520,9 @@ If you intended to add a new pCloud account, tap on this url www.pcloud.com, log out from the current account and tap again on the \'+\' in this app to create a new cloud connection. Vault password migration required - Due to security enhancements, you will be prompted for your biometric authentication multiple times to re-encrypt your vault passwords. This is necessary to continue to protect your vault passwords with the latest technology. If you do not wish this to happen, the stored passwords will be removed. + Due to security enhancements, you will be asked for your biometric authentication twice for each vault to re-encrypt your vault passwords. This is necessary to continue to protect your vault passwords with the latest technology. If you do not wish this to happen, the stored passwords will be removed. Migrate - @string/dialog_button_cancel + @string/screen_vault_list_vault_action_delete Cryptomator needs storage access to use local vaults Cryptomator needs storage access to use auto photo upload @@ -565,6 +565,10 @@ Log in using your biometric credential Use vault password + @string/dialog_biometric_auth_title + @string/dialog_biometric_auth_message + @string/dialog_biometric_auth_use_password + Unable to auto upload files From 827f35d1c0bfed9b155719d03362003ebde9ae4d Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 13 Aug 2024 14:18:11 +0200 Subject: [PATCH 5/7] Run fluidattacks lane during dryRun as well --- fastlane/Fastfile | 43 +++++++++++++++++++++++++++++++++---------- fastlane/README.md | 8 -------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 10a73c3e2..dee6e1c1e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -295,7 +295,7 @@ platform :android do |options| desc "Deploy new lite version" private_lane :deployLite do |options| sh("docker build -t cryptomator-android ../buildsystem") - sh("docker run --rm -u $(id -u):$(id -g) -v $(cd .. && pwd):/project -w /project cryptomator-android ./gradlew clean assembleLiteRelease") + sh("docker run --rm -v $(cd .. && pwd):/project -w /project cryptomator-android ./gradlew clean assembleLiteRelease") sh("zipalign -v -p 4 ../presentation/build/outputs/apk/lite/release/presentation-lite-release-unsigned.apk presentation-lite-release-unsigned-aligned.apk") sh("apksigner sign --ks #{ENV["SIGNING_KEYSTORE_PATH"]} --ks-key-alias #{ENV["SIGNING_KEY_ALIAS"]} --ks-pass env:SIGNING_KEYSTORE_PASSWORD --key-pass env:SIGNING_KEY_PASSWORD --out release/Cryptomator-#{version}_lite_signed.apk presentation-lite-release-unsigned-aligned.apk") @@ -366,20 +366,23 @@ platform :android do |options| desc "Run fluidattacks" lane :runFluidattacks do |options| - # if you want to run it for a specific version just set e.g. version = "1.10.0" - fluidattacks_apks_path = "fluidattacks/apks" - apk_types = %w[signed fdroid_signed lite_signed playstore_signed] - - FileUtils.mkdir("#{fluidattacks_apks_path}") - apk_types.each do |type| - FileUtils.mkdir("#{fluidattacks_apks_path}/Cryptomator-#{version}_#{type}/") - FileUtils.cp("release/Cryptomator-#{version}_#{type}.apk", "#{fluidattacks_apks_path}/Cryptomator-#{version}_#{type}/") + if !options[:verifyOnly] + fluidattacks_apks_path = "fluidattacks/apks" + apk_types = %w[signed fdroid_signed lite_signed playstore_signed] + + FileUtils.mkdir("#{fluidattacks_apks_path}") + apk_types.each do |type| + FileUtils.mkdir("#{fluidattacks_apks_path}/Cryptomator-#{version}_#{type}/") + FileUtils.cp("release/Cryptomator-#{version}_#{type}.apk", "#{fluidattacks_apks_path}/Cryptomator-#{version}_#{type}/") + end end puts "Run Fluidattacks. Results are in /src/fastlane/fluidattacks/results.csv" sh("docker run -v $(cd .. && pwd):/src -w /src fluidattacks/cli:amd64 skims scan /src/fastlane/fluidattacks/config.yaml") - FileUtils.rm_r("#{fluidattacks_apks_path}") + if !options[:verifyOnly] + FileUtils.rm_r("#{fluidattacks_apks_path}") + end end desc "Create GitHub draft release" @@ -419,6 +422,14 @@ platform :android do |options| checkVersionCodeSet(alpha:options[:alpha], beta:options[:beta]) + fluidattacks_apks_path = "fluidattacks/apks" + apk_types = %w[signed fdroid_signed lite_signed playstore_signed] + + FileUtils.mkdir("#{fluidattacks_apks_path}") + apk_types.each do |type| + FileUtils.mkdir("#{fluidattacks_apks_path}/Cryptomator-#{version}_#{type}/") + end + gradle(task: "clean") gradle( @@ -437,6 +448,8 @@ platform :android do |options| checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'playstore') checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'playstore') + FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "#{fluidattacks_apks_path}/Cryptomator-#{version}_playstore_signed/Cryptomator-#{version}_playstore_signed.apk") + gradle(task: "clean") gradle( @@ -455,6 +468,8 @@ platform :android do |options| checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'apkstore') checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'apkstore') + FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "#{fluidattacks_apks_path}/Cryptomator-#{version}_signed/Cryptomator-#{version}_signed.apk") + gradle(task: "clean") gradle( @@ -473,6 +488,8 @@ platform :android do |options| checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'fdroid') checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'fdroid') + FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "#{fluidattacks_apks_path}/Cryptomator-#{version}_fdroid_signed/Cryptomator-#{version}_fdroid_signed.apk") + gradle(task: "clean") gradle( @@ -490,5 +507,11 @@ platform :android do |options| checkTrackingAddedInDependencyUsingIzzyScript(alpha:options[:alpha], beta:options[:beta], flavor: 'lite') checkTrackingAddedInDependencyUsingExodus(alpha:options[:alpha], beta:options[:beta], flavor: 'lite') + + FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "#{fluidattacks_apks_path}/Cryptomator-#{version}_lite_signed/Cryptomator-#{version}_lite_signed.apk") + + runFluidattacks(verifyOnly:true) + + FileUtils.rm_r("#{fluidattacks_apks_path}") end end diff --git a/fastlane/README.md b/fastlane/README.md index 573a02213..c160d8349 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -55,14 +55,6 @@ Update Metadata Check if the version code was set -### android deployToLenotraAG - -```sh -[bundle exec] fastlane android deployToLenotraAG -``` - -Deploy new version to Lenotra AG - ### android checkTrackingAddedInDependencyUsingIzzyScript ```sh From 8b0a6dc34989534c12849be17d8feb066eddcfd5 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Tue, 13 Aug 2024 16:03:07 +0200 Subject: [PATCH 6/7] Apply code review suggestions --- .../ui/dialog/CBCPasswordVaultsMigrationDialog.kt | 10 +++++----- .../cryptomator/util/crypto/CryptoByteArrayUtils.java | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt index 3f3d8078e..12e55a466 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/CBCPasswordVaultsMigrationDialog.kt @@ -36,11 +36,11 @@ class CBCPasswordVaultsMigrationDialog : BaseDialog): DialogFragment { - val dialog = CBCPasswordVaultsMigrationDialog() - val args = Bundle() - args.putSerializable(VAULTS_ARG, ArrayList(cbcVaults)) - dialog.arguments = args - return dialog + return CBCPasswordVaultsMigrationDialog().apply { + arguments = Bundle().apply { + putSerializable(VAULTS_ARG, ArrayList(cbcVaults)) + } + } } } } diff --git a/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java b/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java index 099389318..8b25c2d4f 100644 --- a/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java @@ -3,12 +3,18 @@ public class CryptoByteArrayUtils { public static byte[] getBytes(byte[] encryptedBytesWithIv, int ivLength) { + if (encryptedBytesWithIv == null) { + throw new IllegalArgumentException("Input array must not be null"); + } byte[] bytes = new byte[encryptedBytesWithIv.length - ivLength]; System.arraycopy(encryptedBytesWithIv, ivLength, bytes, 0, bytes.length); return bytes; } public static byte[] join(byte[] encrypted, byte[] iv) { + if (encrypted == null || iv == null) { + throw new IllegalArgumentException("Input arrays must not be null"); + } byte[] result = new byte[iv.length + encrypted.length]; System.arraycopy(iv, 0, result, 0, iv.length); System.arraycopy(encrypted, 0, result, iv.length, encrypted.length); From 7a7f477822aca0174178c3c2b98641a56ec14b54 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Wed, 14 Aug 2024 10:00:01 +0200 Subject: [PATCH 7/7] Store local storage URL in URL property of the cloud entity --- .../data/db/UpgradeDatabaseTest.kt | 34 +++++++++++++++---- .../org/cryptomator/data/db/Upgrade12To13.kt | 14 +++++++- .../data/db/mappers/CloudEntityMapper.java | 4 +-- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt b/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt index d72e0ce6b..239161998 100644 --- a/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt +++ b/data/src/androidTest/java/org/cryptomator/data/db/UpgradeDatabaseTest.kt @@ -674,7 +674,7 @@ class UpgradeDatabaseTest { .integer("FOLDER_CLOUD_ID", 15) // .text("FOLDER_PATH", "path") // .text("FOLDER_NAME", "name") // - .text("CLOUD_TYPE", CloudType.LOCAL.name) // + .text("CLOUD_TYPE", CloudType.DROPBOX.name) // .text("PASSWORD", "password") // .integer("POSITION", 10) // .integer("FORMAT", 8) // @@ -683,7 +683,7 @@ class UpgradeDatabaseTest { Sql.insertInto("CLOUD_ENTITY") // .integer("_id", 15) // - .text("TYPE", CloudType.LOCAL.name) // + .text("TYPE", CloudType.DROPBOX.name) // .text("URL", "url") // .text("USERNAME", "username") // .text("WEBDAV_CERTIFICATE", "certificate") // @@ -698,7 +698,7 @@ class UpgradeDatabaseTest { .integer("FOLDER_CLOUD_ID", 3015) // .text("FOLDER_PATH", "path") // .text("FOLDER_NAME", "name") // - .text("CLOUD_TYPE", CloudType.LOCAL.name) // + .text("CLOUD_TYPE", CloudType.DROPBOX.name) // .text("PASSWORD", null) // .integer("POSITION", 10) // .integer("FORMAT", 8) // @@ -707,7 +707,7 @@ class UpgradeDatabaseTest { Sql.insertInto("CLOUD_ENTITY") // .integer("_id", 3015) // - .text("TYPE", CloudType.LOCAL.name) // + .text("TYPE", CloudType.DROPBOX.name) // .text("URL", "url") // .text("USERNAME", "username") // .text("WEBDAV_CERTIFICATE", "certificate") // @@ -717,6 +717,18 @@ class UpgradeDatabaseTest { .text("S3_SECRET_KEY", null) // .executeOn(db) + Sql.insertInto("CLOUD_ENTITY") // + .integer("_id", 30015) // + .text("TYPE", CloudType.LOCAL.name) // + .text("URL", "url") // + .text("USERNAME", "username") // + .text("WEBDAV_CERTIFICATE", "certificate") // + .text("ACCESS_TOKEN", "testUrl3000") + .text("S3_BUCKET", "s3Bucket") // + .text("S3_REGION", "s3Region") // + .text("S3_SECRET_KEY", null) // + .executeOn(db) + Upgrade12To13(context).applyTo(db, 12) Sql.query("VAULT_ENTITY").where("_id", Sql.eq(25)).executeOn(db).use { @@ -726,7 +738,7 @@ class UpgradeDatabaseTest { Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_PATH")), CoreMatchers.`is`("path")) Assert.assertThat(it.getString(it.getColumnIndex("FOLDER_NAME")), CoreMatchers.`is`("name")) - Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.LOCAL.name)) + Assert.assertThat(it.getString(it.getColumnIndex("CLOUD_TYPE")), CoreMatchers.`is`(CloudType.DROPBOX.name)) Assert.assertThat(it.getInt(it.getColumnIndex("POSITION")), CoreMatchers.`is`(10)) Assert.assertThat(it.getInt(it.getColumnIndex("FORMAT")), CoreMatchers.`is`(8)) Assert.assertThat(it.getInt(it.getColumnIndex("SHORTENING_THRESHOLD")), CoreMatchers.`is`(4)) @@ -739,7 +751,7 @@ class UpgradeDatabaseTest { Assert.assertThat(gcmCryptor.decrypt(it.getString(it.getColumnIndex("S3_SECRET_KEY"))), CoreMatchers.`is`(s3SecretPlain)) Assert.assertThat(it.getString(it.getColumnIndex("S3_SECRET_KEY_CRYPTO_MODE")), CoreMatchers.`is`(CryptoMode.GCM.name)) - Assert.assertThat(it.getString(it.getColumnIndex("TYPE")), CoreMatchers.`is`(CloudType.LOCAL.name)) + Assert.assertThat(it.getString(it.getColumnIndex("TYPE")), CoreMatchers.`is`(CloudType.DROPBOX.name)) Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`("url")) Assert.assertThat(it.getString(it.getColumnIndex("USERNAME")), CoreMatchers.`is`("username")) Assert.assertThat(it.getString(it.getColumnIndex("WEBDAV_CERTIFICATE")), CoreMatchers.`is`("certificate")) @@ -760,5 +772,15 @@ class UpgradeDatabaseTest { Assert.assertNull(it.getString(it.getColumnIndex("S3_SECRET_KEY"))) Assert.assertNull(it.getString(it.getColumnIndex("S3_SECRET_KEY_CRYPTO_MODE"))) } + + Sql.query("CLOUD_ENTITY").where("_id", Sql.eq(30015)).executeOn(db).use { + it.moveToFirst() + Assert.assertThat(it.getString(it.getColumnIndex("URL")), CoreMatchers.`is`("testUrl3000")) + Assert.assertNull(it.getString(it.getColumnIndex("ACCESS_TOKEN"))) + + Assert.assertNull(it.getString(it.getColumnIndex("ACCESS_TOKEN_CRYPTO_MODE"))) + Assert.assertNull(it.getString(it.getColumnIndex("S3_SECRET_KEY"))) + Assert.assertNull(it.getString(it.getColumnIndex("S3_SECRET_KEY_CRYPTO_MODE"))) + } } } diff --git a/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt index bc576c8b3..a7da4a7b6 100644 --- a/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt @@ -1,7 +1,6 @@ package org.cryptomator.data.db import android.content.Context -import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.crypto.CredentialCryptor import org.cryptomator.util.crypto.CryptoMode import org.greenrobot.greendao.database.Database @@ -15,6 +14,7 @@ internal class Upgrade12To13 @Inject constructor(private val context: Context) : override fun internalApplyTo(db: Database, origin: Int) { db.beginTransaction() try { + moveLocalStorageUrlToUrlProperty(db) addCryptoModeToDbEntities(db) applyVaultPasswordCryptoModeToDb(db) upgradeCloudCryptoModeToGCM(db) @@ -24,6 +24,18 @@ internal class Upgrade12To13 @Inject constructor(private val context: Context) : } } + private fun moveLocalStorageUrlToUrlProperty(db: Database) { + Sql.query("CLOUD_ENTITY").where("TYPE", Sql.eq("LOCAL")).executeOn(db).use { + while (it.moveToNext()) { + Sql.update("CLOUD_ENTITY") // + .where("_id", Sql.eq(it.getLong(it.getColumnIndex("_id")))) // + .set("URL", Sql.toString(it.getString(it.getColumnIndex("ACCESS_TOKEN")))) // + .set("ACCESS_TOKEN", Sql.toNull()) // + .executeOn(db) + } + } + } + private fun addCryptoModeToDbEntities(db: Database) { Sql.alterTable("CLOUD_ENTITY").renameTo("CLOUD_ENTITY_OLD").executeOn(db) diff --git a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java index 70e049f7a..4d10a3ab5 100644 --- a/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java +++ b/data/src/main/java/org/cryptomator/data/db/mappers/CloudEntityMapper.java @@ -48,7 +48,7 @@ public Cloud fromEntity(CloudEntity entity) { case LOCAL: return aLocalStorage() // .withId(entity.getId()) // - .withRootUri(entity.getAccessToken()).build(); + .withRootUri(entity.getUrl()).build(); case ONEDRIVE: return aOnedriveCloud() // .withId(entity.getId()) // @@ -100,7 +100,7 @@ public CloudEntity toEntity(Cloud domainObject) { result.setUsername(((GoogleDriveCloud) domainObject).username()); break; case LOCAL: - result.setAccessToken(((LocalStorageCloud) domainObject).rootUri()); + result.setUrl(((LocalStorageCloud) domainObject).rootUri()); break; case ONEDRIVE: result.setAccessToken(((OnedriveCloud) domainObject).accessToken());