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..239161998 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,143 @@ 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.DROPBOX.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.DROPBOX.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.DROPBOX.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.DROPBOX.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) + + 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 { + 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.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)) + } + + 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.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")) + 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"))) + } + + 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/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..a7da4a7b6 --- /dev/null +++ b/data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt @@ -0,0 +1,142 @@ +package org.cryptomator.data.db + +import android.content.Context +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 +import timber.log.Timber + +@Singleton +internal class Upgrade12To13 @Inject constructor(private val context: Context) : DatabaseUpgrade(12, 13) { + + override fun internalApplyTo(db: Database, origin: Int) { + db.beginTransaction() + try { + moveLocalStorageUrlToUrlProperty(db) + addCryptoModeToDbEntities(db) + applyVaultPasswordCryptoModeToDb(db) + upgradeCloudCryptoModeToGCM(db) + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + 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) + + 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 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) { + 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.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.toString())) // + .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) { 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()); 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 b38f4ea69..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,40 +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 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); - - 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/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 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..12e55a466 --- /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 { + return CBCPasswordVaultsMigrationDialog().apply { + arguments = Bundle().apply { + putSerializable(VAULTS_ARG, ArrayList(cbcVaults)) + } + } + } + } +} 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..7b63b78ef 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthentication.kt @@ -45,7 +45,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) } @@ -81,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..4913b42dd --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/util/BiometricAuthenticationMigration.kt @@ -0,0 +1,142 @@ +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 +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( + 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) + } + + 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_migration_auth_title)) // + .setSubtitle(context.getString(R.string.dialog_biometric_migration_auth_message)) // + .setConfirmationRequired(useConfirmationInFaceUnlockAuth) // + .setNegativeButtonText(context.getString(R.string.dialog_biometric_migration_auth_use_password)) // + .build() + + processNextVault(fragment, vaultQueue, decryptedVaults, reEncryptedVaults, vaultModels) + } + + 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) + } + } + + @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 biometricAuthCryptor = BiometricAuthCryptor.getInstance(context, cryptoMode) + val authCallback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Timber.tag("BiometricAuthMigration").d("Authentication succeeded") + val cipher = result.cryptoObject?.cipher + try { + 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.e(e, "BadPaddingException - possibly due to an invalidated key") + callback.onBiometricKeyInvalidated(allVaults) + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Timber.e("Authentication error: %s errorCode=%d", errString, errorCode) + callback.onBiometricAuthenticationFailed(allVaults) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Timber.e("Authentication failed") + } + } + val biometricPrompt = BiometricPrompt(fragment, ContextCompat.getMainExecutor(context), authCallback) + val cryptoCipher = when (cryptoMode) { + CryptoMode.CBC -> biometricAuthCryptor.getDecryptCipher(password) + CryptoMode.GCM -> biometricAuthCryptor.encryptCipher + CryptoMode.NONE -> throw IllegalStateException("CryptoMode.NONE is not allowed here") + } + biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cryptoCipher)) + } catch (e: KeyPermanentlyInvalidatedException) { + 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/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..55bee9500 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 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/screen_vault_list_vault_action_delete + 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 @@ -560,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 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/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..8b25c2d4f --- /dev/null +++ b/util/src/main/java/org/cryptomator/util/crypto/CryptoByteArrayUtils.java @@ -0,0 +1,23 @@ +package org.cryptomator.util.crypto; + +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); + 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..02848382c --- /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, NONE; +} 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) {