Skip to content

Commit

Permalink
Merge pull request #547 from cryptomator/feature/migrate-to-gcm
Browse files Browse the repository at this point in the history
Migrate vault passwords to GCM
  • Loading branch information
SailReal authored Aug 14, 2024
2 parents 5c5e45f + 7a7f477 commit 7a37c03
Show file tree
Hide file tree
Showing 43 changed files with 1,114 additions and 146 deletions.
2 changes: 1 addition & 1 deletion data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ android {
}

greendao {
schemaVersion 12
schemaVersion 13
}

configurations.all {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public DatabaseUpgrades( //
Upgrade8To9 upgrade8To9, //
Upgrade9To10 upgrade9To10, //
Upgrade10To11 upgrade10To11, //
Upgrade11To12 upgrade11To12
Upgrade11To12 upgrade11To12, //
Upgrade12To13 upgrade12To13
) {

availableUpgrades = defineUpgrades( //
Expand All @@ -45,7 +46,8 @@ public DatabaseUpgrades( //
upgrade8To9, //
upgrade9To10, //
upgrade10To11, //
upgrade11To12);
upgrade11To12, //
upgrade12To13);
}

private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {
Expand Down
4 changes: 4 additions & 0 deletions data/src/main/java/org/cryptomator/data/db/Sql.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
142 changes: 142 additions & 0 deletions data/src/main/java/org/cryptomator/data/db/Upgrade12To13.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class CloudEntity extends DatabaseEntity {

private String accessToken;

private String accessTokenCryptoMode;

private String url;

private String username;
Expand All @@ -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)
Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit 7a37c03

Please sign in to comment.