diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index b03c0a569..6bddfec20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob; import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob; import org.thoughtcrime.securesms.migrations.StickerLaunchMigrationJob; +import org.thoughtcrime.securesms.migrations.StorageKeyRotationMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob; import org.thoughtcrime.securesms.migrations.UuidMigrationJob; @@ -117,6 +118,7 @@ public final class JobManagerFactories { put(RegistrationPinV2MigrationJob.KEY, new RegistrationPinV2MigrationJob.Factory()); put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory()); put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory()); + put(StorageKeyRotationMigrationJob.KEY, new StorageKeyRotationMigrationJob.Factory()); put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index 8e0f0f794..273194421 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.InvalidKeyException; @@ -28,6 +29,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -77,10 +79,6 @@ public class StorageForcePushJob extends BaseJob { long currentVersion = accountManager.getStorageManifestVersion(); Map oldStorageKeys = recipientDatabase.getAllStorageSyncKeysMap(); - if (currentVersion < 1) { - throw new IllegalStateException("We should never be force-pushing a manifest as the first version!"); - } - long newVersion = currentVersion + 1; Map newStorageKeys = generateNewKeys(oldStorageKeys); List inserts = Stream.of(oldStorageKeys.keySet()) @@ -92,10 +90,18 @@ public class StorageForcePushJob extends BaseJob { SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values())); try { - Log.i(TAG, String.format(Locale.ENGLISH, "Force-pushing data. Inserting %d keys.", inserts.size())); - if (accountManager.resetStorageRecords(storageServiceKey, manifest, inserts).isPresent()) { - Log.w(TAG, "Hit a conflict. Trying again."); - throw new RetryLaterException(); + if (newVersion > 1) { + Log.i(TAG, String.format(Locale.ENGLISH, "Force-pushing data. Inserting %d keys.", inserts.size())); + if (accountManager.resetStorageRecords(storageServiceKey, manifest, inserts).isPresent()) { + Log.w(TAG, "Hit a conflict. Trying again."); + throw new RetryLaterException(); + } + } else { + Log.i(TAG, String.format(Locale.ENGLISH, "First version, normal push. Inserting %d keys.", inserts.size())); + if (accountManager.writeStorageRecords(storageServiceKey, manifest, inserts, Collections.emptyList()).isPresent()) { + Log.w(TAG, "Hit a conflict. Trying again."); + throw new RetryLaterException(); + } } } catch (InvalidKeyException e) { Log.w(TAG, "Hit an invalid key exception, which likely indicates a conflict."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index baeb03a35..987a66489 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -92,7 +92,10 @@ public class StorageSyncJob extends BaseJob { @Override protected void onRun() throws IOException, RetryLaterException { - if (!FeatureFlags.storageService()) return; + if (!FeatureFlags.storageService()) { + Log.i(TAG, "Not enabled. Skipping."); + return; + } try { boolean needsMultiDeviceSync = performSync(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java index a7f083a34..0d5cf65e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java @@ -32,6 +32,12 @@ public class StorageServiceValues { return new MasterKey(blob); } + public synchronized void rotateStorageMasterKey() { + store.beginWrite() + .putBlob(STORAGE_MASTER_KEY, MasterKey.createNew(new SecureRandom()).serialize()) + .commit(); + } + public boolean hasFirstStorageSyncCompleted() { return !FeatureFlags.storageServiceRestore() || store.getBoolean(FIRST_STORAGE_SYNC_COMPLETED, true); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 434ec595f..10f7a2934 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -40,7 +40,7 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - public static final int CURRENT_VERSION = 11; + public static final int CURRENT_VERSION = 12; private static final class Version { static final int LEGACY = 1; @@ -54,6 +54,7 @@ public class ApplicationMigrations { static final int TEST_ARGON2 = 9; static final int SWOON_STICKERS = 10; static final int STORAGE_SERVICE = 11; + static final int STORAGE_KEY_ROTATE = 12; } /** @@ -210,6 +211,10 @@ public class ApplicationMigrations { jobs.put(Version.STORAGE_SERVICE, new StorageServiceMigrationJob()); } + if (lastSeenVersion < Version.STORAGE_KEY_ROTATE) { + jobs.put(Version.STORAGE_KEY_ROTATE, new StorageKeyRotationMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageKeyRotationMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageKeyRotationMigrationJob.java new file mode 100644 index 000000000..3f5887a00 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageKeyRotationMigrationJob.java @@ -0,0 +1,69 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob; +import org.thoughtcrime.securesms.jobs.MultiDeviceStorageSyncRequestJob; +import org.thoughtcrime.securesms.jobs.StorageForcePushJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +public class StorageKeyRotationMigrationJob extends MigrationJob { + + private static final String TAG = Log.tag(StorageKeyRotationMigrationJob.class); + + public static final String KEY = "StorageKeyRotationMigrationJob"; + + StorageKeyRotationMigrationJob() { + this(new Parameters.Builder().build()); + } + + private StorageKeyRotationMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() { + JobManager jobManager = ApplicationDependencies.getJobManager(); + SignalStore.storageServiceValues().rotateStorageMasterKey(); + + if (TextSecurePreferences.isMultiDevice(context)) { + Log.i(TAG, "Multi-device."); + jobManager.startChain(new StorageForcePushJob()) + .then(new MultiDeviceKeysUpdateJob()) + .then(new MultiDeviceStorageSyncRequestJob()) + .enqueue(); + } else { + Log.i(TAG, "Single-device."); + jobManager.add(new StorageForcePushJob()); + } + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull StorageKeyRotationMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new StorageKeyRotationMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java index 27b42ba7b..4b1bac686 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java @@ -48,8 +48,8 @@ public class StorageServiceMigrationJob extends MigrationJob { if (TextSecurePreferences.isMultiDevice(context)) { Log.i(TAG, "Multi-device."); - jobManager.startChain(new MultiDeviceKeysUpdateJob()) - .then(new StorageSyncJob()) + jobManager.startChain(new StorageSyncJob()) + .then(new MultiDeviceKeysUpdateJob()) .enqueue(); } else { Log.i(TAG, "Single-device."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 201d3c02e..c703c0823 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -53,7 +53,7 @@ public final class FeatureFlags { private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch"; private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone"; private static final String VIDEO_TRIMMING = "android.videoTrimming"; - private static final String STORAGE_SERVICE = "android.storageService"; + private static final String STORAGE_SERVICE = "android.storageService.2"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageCipher.java index c9360e888..c1b0055cb 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageCipher.java @@ -18,10 +18,12 @@ import javax.crypto.spec.SecretKeySpec; */ public class SignalStorageCipher { + private static final int IV_LENGTH = 12; + public static byte[] encrypt(StorageCipherKey key, byte[] data) { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - byte[] iv = Util.getSecretBytes(16); + byte[] iv = Util.getSecretBytes(IV_LENGTH); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, iv)); byte[] ciphertext = cipher.doFinal(data); @@ -35,7 +37,7 @@ public class SignalStorageCipher { public static byte[] decrypt(StorageCipherKey key, byte[] data) throws InvalidKeyException { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - byte[][] split = Util.split(data, 16, data.length - 16); + byte[][] split = Util.split(data, IV_LENGTH, data.length - IV_LENGTH); byte[] iv = split[0]; byte[] cipherText = split[1];