diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index 46933f55c8..14a7802359 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false, true, true)) SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.markRegistered(recipientId, aci) val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair() diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt index e8cde2434f..88621104ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.kt @@ -8,11 +8,12 @@ object AppCapabilities { * asking if the user has set a Signal PIN or not. */ @JvmStatic - fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities { + fun getCapabilities(storageCapable: Boolean, storageServiceEncryptionV2: Boolean): AccountAttributes.Capabilities { return AccountAttributes.Capabilities( storage = storageCapable, deleteSync = true, - versionedExpirationTimer = true + versionedExpirationTimer = true, + storageServiceEncryptionV2 = storageServiceEncryptionV2 ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index bb031135ad..3e1d5549e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -420,6 +420,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da var value: Long = 0 value = Bitmask.update(value, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isDeleteSync).serialize().toLong()) value = Bitmask.update(value, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isVersionedExpirationTimer).serialize().toLong()) + value = Bitmask.update(value, Capabilities.STORAGE_SERVICE_ENCRYPTION_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStorageServiceEncryptionV2).serialize().toLong()) return value } } @@ -4713,6 +4714,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da // const val PAYMENT_ACTIVATION = 8 const val DELETE_SYNC = 9 const val VERSIONED_EXPIRATION_TIMER = 10 + const val STORAGE_SERVICE_ENCRYPTION_V2 = 11 // IMPORTANT: We cannot sore more than 32 capabilities in the bitmask. } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt index 61b346682d..d66dc1c3e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTableCursorUtil.kt @@ -177,7 +177,8 @@ object RecipientTableCursorUtil { return RecipientRecord.Capabilities( rawBits = capabilities, deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.DELETE_SYNC, Capabilities.BIT_LENGTH).toInt()), - versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH).toInt()) + versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.VERSIONED_EXPIRATION_TIMER, Capabilities.BIT_LENGTH).toInt()), + storageServiceEncryptionV2 = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORAGE_SERVICE_ENCRYPTION_V2, Capabilities.BIT_LENGTH).toInt()) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 2168cd3c1a..6816a39a1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -121,14 +121,16 @@ data class RecipientRecord( data class Capabilities( val rawBits: Long, val deleteSync: Recipient.Capability, - val versionedExpirationTimer: Recipient.Capability + val versionedExpirationTimer: Recipient.Capability, + val storageServiceEncryptionV2: Recipient.Capability ) { companion object { @JvmField val UNKNOWN = Capabilities( rawBits = 0, deleteSync = Recipient.Capability.UNKNOWN, - versionedExpirationTimer = Recipient.Capability.UNKNOWN + versionedExpirationTimer = Recipient.Capability.UNKNOWN, + storageServiceEncryptionV2 = Recipient.Capability.UNKNOWN ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index fe17673554..778e081aff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -52,6 +52,7 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi import org.whispersystems.signalservice.api.services.CallLinksService import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.ProfileService +import org.whispersystems.signalservice.api.storage.StorageServiceApi import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.push.PushServiceSocket @@ -303,6 +304,9 @@ object AppDependencies { val registrationApi: RegistrationApi get() = networkModule.registrationApi + val storageServiceApi: StorageServiceApi + get() = networkModule.storageServiceApi + @JvmStatic val okHttpClient: OkHttpClient get() = networkModule.okHttpClient @@ -367,5 +371,6 @@ object AppDependencies { fun provideAttachmentApi(signalWebSocket: SignalWebSocket, pushServiceSocket: PushServiceSocket): AttachmentApi fun provideLinkDeviceApi(pushServiceSocket: PushServiceSocket): LinkDeviceApi fun provideRegistrationApi(pushServiceSocket: PushServiceSocket): RegistrationApi + fun provideStorageServiceApi(pushServiceSocket: PushServiceSocket): StorageServiceApi } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 4ab7193908..1fff6ff1f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -94,6 +94,7 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.CallLinksService; import org.whispersystems.signalservice.api.services.DonationsService; import org.whispersystems.signalservice.api.services.ProfileService; +import org.whispersystems.signalservice.api.storage.StorageServiceApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -244,6 +245,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { public @NonNull Network provideLibsignalNetwork(@NonNull SignalServiceConfiguration config) { Network network = new Network(BuildConfig.LIBSIGNAL_NET_ENV, StandardUserAgentInterceptor.USER_AGENT); LibSignalNetworkExtensions.applyConfiguration(network, config); + return network; } @@ -480,6 +482,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { return new RegistrationApi(pushServiceSocket); } + @Override + public @NonNull StorageServiceApi provideStorageServiceApi(@NonNull PushServiceSocket pushServiceSocket) { + return new StorageServiceApi(pushServiceSocket); + } + @VisibleForTesting static class DynamicCredentialsProvider implements CredentialsProvider { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt index 946b37b4d8..0e253e5b47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt @@ -37,6 +37,7 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi import org.whispersystems.signalservice.api.services.CallLinksService import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.ProfileService +import org.whispersystems.signalservice.api.storage.StorageServiceApi import org.whispersystems.signalservice.api.util.Tls12SocketFactory import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import org.whispersystems.signalservice.internal.push.PushServiceSocket @@ -148,6 +149,10 @@ class NetworkDependenciesModule( provider.provideRegistrationApi(pushServiceSocket) } + val storageServiceApi: StorageServiceApi by lazy { + provider.provideStorageServiceApi(pushServiceSocket) + } + val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .addInterceptor(StandardUserAgentInterceptor()) 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 1a6003b7cd..7ff3d3bfa4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigra import org.thoughtcrime.securesms.jobmanager.migrations.SenderKeyDistributionSendJobRecipientMigration; import org.thoughtcrime.securesms.migrations.AccountConsistencyMigrationJob; import org.thoughtcrime.securesms.migrations.AccountRecordMigrationJob; +import org.thoughtcrime.securesms.migrations.AepMigrationJob; import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJob; import org.thoughtcrime.securesms.migrations.AttachmentCleanupMigrationJob; import org.thoughtcrime.securesms.migrations.AttachmentHashBackfillMigrationJob; @@ -136,7 +137,6 @@ public final class JobManagerFactories { put(CheckRestoreMediaLeftJob.KEY, new CheckRestoreMediaLeftJob.Factory()); put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); - put(ContactLinkRebuildMigrationJob.KEY, new ContactLinkRebuildMigrationJob.Factory()); put(ConversationShortcutRankingUpdateJob.KEY, new ConversationShortcutRankingUpdateJob.Factory()); put(ConversationShortcutUpdateJob.KEY, new ConversationShortcutUpdateJob.Factory()); put(CopyAttachmentToArchiveJob.KEY, new CopyAttachmentToArchiveJob.Factory()); @@ -238,6 +238,7 @@ public final class JobManagerFactories { put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); + put(StorageRotateManifestJob.KEY, new StorageRotateManifestJob.Factory()); put(SyncSystemContactLinksJob.KEY, new SyncSystemContactLinksJob.Factory()); put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory()); put(ResetSvrGuessCountJob.KEY, new ResetSvrGuessCountJob.Factory()); @@ -247,7 +248,6 @@ public final class JobManagerFactories { put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory()); put(StorageSyncJob.KEY, new StorageSyncJob.Factory()); - put(SubscriberIdMigrationJob.KEY, new SubscriberIdMigrationJob.Factory()); put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory()); put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory()); put(Svr2MirrorJob.KEY, new Svr2MirrorJob.Factory()); @@ -261,6 +261,7 @@ public final class JobManagerFactories { // Migrations put(AccountConsistencyMigrationJob.KEY, new AccountConsistencyMigrationJob.Factory()); put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory()); + put(AepMigrationJob.KEY, new AepMigrationJob.Factory()); put(ApplyUnknownFieldsToSelfMigrationJob.KEY, new ApplyUnknownFieldsToSelfMigrationJob.Factory()); put(AttachmentCleanupMigrationJob.KEY, new AttachmentCleanupMigrationJob.Factory()); put(AttachmentHashBackfillMigrationJob.KEY, new AttachmentHashBackfillMigrationJob.Factory()); @@ -275,6 +276,7 @@ public final class JobManagerFactories { put(BlobStorageLocationMigrationJob.KEY, new BlobStorageLocationMigrationJob.Factory()); put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory()); put(ClearGlideCacheMigrationJob.KEY, new ClearGlideCacheMigrationJob.Factory()); + put(ContactLinkRebuildMigrationJob.KEY, new ContactLinkRebuildMigrationJob.Factory()); put(CopyUsernameToSignalStoreMigrationJob.KEY, new CopyUsernameToSignalStoreMigrationJob.Factory()); put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory()); put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory()); @@ -306,6 +308,7 @@ public final class JobManagerFactories { put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory()); put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory()); + put(SubscriberIdMigrationJob.KEY, new SubscriberIdMigrationJob.Factory()); put(Svr2MirrorMigrationJob.KEY, new Svr2MirrorMigrationJob.Factory()); put(SyncCallLinksMigrationJob.KEY, new SyncCallLinksMigrationJob.Factory()); put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.kt index 952dd00dce..2aff7c077d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.kt @@ -13,7 +13,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException import java.io.IOException -import java.util.Optional class MultiDeviceKeysUpdateJob private constructor(parameters: Parameters) : BaseJob(parameters) { @@ -54,8 +53,10 @@ class MultiDeviceKeysUpdateJob private constructor(parameters: Parameters) : Bas val syncMessage = SignalServiceSyncMessage.forKeys( KeysMessage( - Optional.of(SignalStore.storageService.storageKey), - Optional.of(SignalStore.svr.masterKey) + storageService = SignalStore.storageService.storageKey, + master = SignalStore.svr.masterKey, + accountEntropyPool = SignalStore.account.accountEntropyPool, + mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index e55101f49b..bb2156a843 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SvrValues; import org.thoughtcrime.securesms.registration.data.RegistrationRepository; import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher; +import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; @@ -103,7 +104,7 @@ public class RefreshAttributesJob extends BaseJob { String deviceName = SignalStore.account().getDeviceName(); byte[] encryptedDeviceName = (deviceName == null) ? null : DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), SignalStore.account().getAciIdentityKey()); - AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(svrValues.hasOptedInWithAccess() && !svrValues.hasOptedOut()); + AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(svrValues.hasOptedInWithAccess() && !svrValues.hasOptedOut(), RemoteConfig.getStorageServiceEncryptionV2()); Log.i(TAG, "Calling setAccountAttributes() reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + svrValues.hasPin() + ", access? " + svrValues.hasOptedInWithAccess() + "\n Recovery password? " + !TextUtils.isEmpty(recoveryPassword) + "\n Phone number discoverable : " + phoneNumberDiscoverable + diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index a3b449f42c..4cf87026b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -216,17 +216,25 @@ public class RefreshOwnProfileJob extends BaseJob { return; } - if (!Recipient.self().getDeleteSyncCapability().isSupported() && capabilities.isDeleteSync()) { + Recipient selfSnapshot = Recipient.self(); + + SignalDatabase.recipients().setCapabilities(Recipient.self().getId(), capabilities); + + if (!selfSnapshot.getDeleteSyncCapability().isSupported() && capabilities.isDeleteSync()) { Log.d(TAG, "Transitioned to delete sync capable, notify linked devices in case we were the last one"); AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob()); } - if (!Recipient.self().getVersionedExpirationTimerCapability().isSupported() && capabilities.isVersionedExpirationTimer()) { + if (!selfSnapshot.getVersionedExpirationTimerCapability().isSupported() && capabilities.isVersionedExpirationTimer()) { Log.d(TAG, "Transitioned to versioned expiration timer capable, notify linked devices in case we were the last one"); AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob()); } - SignalDatabase.recipients().setCapabilities(Recipient.self().getId(), capabilities); + if (selfSnapshot.getStorageServiceEncryptionV2Capability() == Recipient.Capability.NOT_SUPPORTED && capabilities.isStorageServiceEncryptionV2()) { + Log.i(TAG, "Transitioned to storageServiceEncryptionV2 capable. Notifying other devices and pushing to storage service with a recordIkm."); + AppDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob()); + AppDependencies.getJobManager().add(new StorageForcePushJob()); + } } private void ensureUnidentifiedAccessCorrect(@Nullable String unidentifiedAccessVerifier, boolean universalUnidentifiedAccess) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.kt index 7940ea3a3a..54b6b1bf83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.kt @@ -6,12 +6,16 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.reclaimUsernameIfNecessary import org.thoughtcrime.securesms.recipients.Recipient.Companion.self import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.storage.SignalAccountRecord import org.whispersystems.signalservice.api.storage.SignalStorageManifest +import org.whispersystems.signalservice.api.storage.SignalStorageRecord +import org.whispersystems.signalservice.api.storage.StorageServiceRepository +import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestResult import java.util.concurrent.TimeUnit /** @@ -43,13 +47,26 @@ class StorageAccountRestoreJob private constructor(parameters: Parameters) : Bas @Throws(Exception::class) override fun onRun() { - val accountManager = AppDependencies.signalServiceAccountManager - val storageServiceKey = SignalStore.storageService.storageKey + val storageServiceKey = SignalStore.storageService.storageKeyForInitialDataRestore?.let { + Log.i(TAG, "Using temporary storage key.") + it + } ?: run { + Log.i(TAG, "Using normal storage key.") + SignalStore.storageService.storageKey + } + + val repository = StorageServiceRepository(SignalNetwork.storageService) Log.i(TAG, "Retrieving manifest...") - val manifest = accountManager.getStorageManifest(storageServiceKey) + val manifest: SignalStorageManifest? = when (val result = repository.getStorageManifest(storageServiceKey)) { + is ManifestResult.Success -> result.manifest + is ManifestResult.DecryptionError -> null + is ManifestResult.NotFoundError -> null + is ManifestResult.NetworkError -> throw result.exception + is ManifestResult.StatusCodeError -> throw result.exception + } - if (!manifest.isPresent) { + if (manifest == null) { Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring. Force-pushing.") AppDependencies.jobManager.add(StorageForcePushJob()) return @@ -58,7 +75,7 @@ class StorageAccountRestoreJob private constructor(parameters: Parameters) : Bas Log.i(TAG, "Resetting the local manifest to an empty state so that it will sync later.") SignalStore.storageService.manifest = SignalStorageManifest.EMPTY - val accountId = manifest.get().accountStorageId + val accountId = manifest.accountStorageId if (!accountId.isPresent) { Log.w(TAG, "Manifest had no account record! Not restoring.") @@ -66,8 +83,18 @@ class StorageAccountRestoreJob private constructor(parameters: Parameters) : Bas } Log.i(TAG, "Retrieving account record...") - val records = accountManager.readStorageRecords(storageServiceKey, listOf(accountId.get())) - val record = if (records.size > 0) records[0] else null + val records: List = when (val result = repository.readStorageRecords(storageServiceKey, manifest.recordIkm, listOf(accountId.get()))) { + is StorageServiceRepository.StorageRecordResult.Success -> result.records + is StorageServiceRepository.StorageRecordResult.DecryptionError -> { + Log.w(TAG, "Account record was undecryptable. Not restoring. Force-pushing.") + AppDependencies.jobManager.add(StorageForcePushJob()) + return + } + is StorageServiceRepository.StorageRecordResult.NetworkError -> throw result.exception + is StorageServiceRepository.StorageRecordResult.StatusCodeError -> throw result.exception + } + + val record = if (records.isNotEmpty()) records[0] else null if (record == null) { Log.w(TAG, "Could not find account record, even though we had an ID! Not restoring.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt index 4bcd89c6c6..8c9a2d55bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.jobs import org.signal.core.util.logging.Log -import org.signal.libsignal.protocol.InvalidKeyException import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job @@ -13,10 +12,13 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.storage.StorageSyncModels import org.thoughtcrime.securesms.storage.StorageSyncValidations import org.thoughtcrime.securesms.transport.RetryLaterException +import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException +import org.whispersystems.signalservice.api.storage.RecordIkm import org.whispersystems.signalservice.api.storage.SignalStorageManifest import org.whispersystems.signalservice.api.storage.SignalStorageRecord import org.whispersystems.signalservice.api.storage.StorageId +import org.whispersystems.signalservice.api.storage.StorageServiceRepository import java.io.IOException import java.util.Collections import java.util.concurrent.TimeUnit @@ -62,9 +64,14 @@ class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob( } val storageServiceKey = SignalStore.storageService.storageKey - val accountManager = AppDependencies.signalServiceAccountManager + val repository = StorageServiceRepository(AppDependencies.storageServiceApi) - val currentVersion = accountManager.storageManifestVersion + val currentVersion = when (val result = repository.getManifestVersion()) { + is NetworkResult.Success -> result.result + is NetworkResult.ApplicationError -> throw result.throwable + is NetworkResult.NetworkError -> throw result.exception + is NetworkResult.StatusCodeError -> throw result.exception + } val oldContactStorageIds: Map = SignalDatabase.recipients.getContactStorageSyncIdsMap() val newVersion = currentVersion + 1 @@ -80,30 +87,44 @@ class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob( inserts.add(accountRecord) allNewStorageIds.add(accountRecord.id) - val manifest = SignalStorageManifest(newVersion, SignalStore.account.deviceId, allNewStorageIds) + val recordIkm: RecordIkm? = if (Recipient.self().storageServiceEncryptionV2Capability.isSupported) { + Log.i(TAG, "Generating and including a new recordIkm.") + RecordIkm.generate() + } else { + Log.i(TAG, "SSRE2 not yet supported. Not including recordIkm.") + null + } + + val manifest = SignalStorageManifest(newVersion, SignalStore.account.deviceId, recordIkm, allNewStorageIds) StorageSyncValidations.validateForcePush(manifest, inserts, Recipient.self().fresh()) - try { - if (newVersion > 1) { - Log.i(TAG, "Force-pushing data. Inserting ${inserts.size} IDs.") - if (accountManager.resetStorageRecords(storageServiceKey, manifest, inserts).isPresent) { - Log.w(TAG, "Hit a conflict. Trying again.") - throw RetryLaterException() - } - } else { - Log.i(TAG, "First version, normal push. Inserting ${inserts.size} IDs.") - if (accountManager.writeStorageRecords(storageServiceKey, manifest, inserts, emptyList()).isPresent) { + if (newVersion > 1) { + Log.i(TAG, "Force-pushing data. Inserting ${inserts.size} IDs.") + when (val result = repository.resetAndWriteStorageRecords(storageServiceKey, manifest, inserts)) { + StorageServiceRepository.WriteStorageRecordsResult.Success -> Unit + is StorageServiceRepository.WriteStorageRecordsResult.StatusCodeError -> throw result.exception + is StorageServiceRepository.WriteStorageRecordsResult.NetworkError -> throw result.exception + StorageServiceRepository.WriteStorageRecordsResult.ConflictError -> { + Log.w(TAG, "Hit a conflict. Trying again.") + throw RetryLaterException() + } + } + } else { + Log.i(TAG, "First version, normal push. Inserting ${inserts.size} IDs.") + when (val result = repository.writeStorageRecords(storageServiceKey, manifest, inserts, emptyList())) { + StorageServiceRepository.WriteStorageRecordsResult.Success -> Unit + is StorageServiceRepository.WriteStorageRecordsResult.StatusCodeError -> throw result.exception + is StorageServiceRepository.WriteStorageRecordsResult.NetworkError -> throw result.exception + is StorageServiceRepository.WriteStorageRecordsResult.ConflictError -> { Log.w(TAG, "Hit a conflict. Trying again.") throw RetryLaterException() } } - } catch (e: InvalidKeyException) { - Log.w(TAG, "Hit an invalid key exception, which likely indicates a conflict.") - throw RetryLaterException(e) } Log.i(TAG, "Force push succeeded. Updating local manifest version to: $newVersion") SignalStore.storageService.manifest = manifest + SignalStore.storageService.storageKeyForInitialDataRestore = null SignalDatabase.recipients.applyStorageIdUpdates(newContactStorageIds) SignalDatabase.recipients.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().id, accountRecord.id)) SignalDatabase.unknownStorageIds.deleteAll() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageRotateManifestJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageRotateManifestJob.kt new file mode 100644 index 0000000000..be28a95517 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageRotateManifestJob.kt @@ -0,0 +1,120 @@ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.whispersystems.signalservice.api.storage.SignalStorageManifest +import org.whispersystems.signalservice.api.storage.StorageKey +import org.whispersystems.signalservice.api.storage.StorageServiceRepository +import java.util.concurrent.TimeUnit + +/** + * After registration, if the user did not restore their AEP, they'll have a new master key and need to write a newly-encrypted manifest. + * If the account is SSRE2-capable, that's all we have to upload. + * If they're not, this job will recognize it and schedule a [StorageForcePushJob] instead. + */ +class StorageRotateManifestJob private constructor(parameters: Parameters) : Job(parameters) { + companion object { + const val KEY: String = "StorageRotateManifestJob" + + private val TAG = Log.tag(StorageRotateManifestJob::class.java) + } + + constructor() : this( + Parameters.Builder().addConstraint(NetworkConstraint.KEY) + .setQueue(StorageSyncJob.QUEUE_KEY) + .setMaxInstancesForFactory(1) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .build() + ) + + override fun serialize(): ByteArray? = null + + override fun getFactoryKey(): String = KEY + + override fun run(): Result { + if (SignalStore.account.isLinkedDevice) { + Log.i(TAG, "Only the primary device can rotate the manifest.") + return Result.failure() + } + + if (!SignalStore.account.isRegistered || SignalStore.account.e164 == null) { + Log.w(TAG, "User not registered. Skipping.") + return Result.failure() + } + + val restoreKey: StorageKey? = SignalStore.storageService.storageKeyForInitialDataRestore + if (restoreKey == null) { + Log.w(TAG, "There was no restore key present! Someone must have written to storage service in the meantime.") + return Result.failure() + } + + val storageServiceKey = SignalStore.storageService.storageKey + val repository = StorageServiceRepository(AppDependencies.storageServiceApi) + + val currentManifest: SignalStorageManifest = when (val result = repository.getStorageManifest(restoreKey)) { + is StorageServiceRepository.ManifestResult.Success -> { + result.manifest + } + is StorageServiceRepository.ManifestResult.DecryptionError -> { + Log.w(TAG, "Failed to decrypt the manifest! Only recourse is to force push.", result.exception) + AppDependencies.jobManager.add(StorageForcePushJob()) + return Result.failure() + } + is StorageServiceRepository.ManifestResult.NetworkError -> { + Log.w(TAG, "Encountered a network error during read, retrying.", result.exception) + return Result.retry(defaultBackoff()) + } + StorageServiceRepository.ManifestResult.NotFoundError -> { + Log.w(TAG, "No existing manifest was found! Force pushing.") + AppDependencies.jobManager.add(StorageForcePushJob()) + return Result.failure() + } + is StorageServiceRepository.ManifestResult.StatusCodeError -> { + Log.w(TAG, "Encountered a status code error during read, retrying.", result.exception) + return Result.retry(defaultBackoff()) + } + } + + if (currentManifest.recordIkm == null) { + Log.w(TAG, "No recordIkm set! Can't just rotate the manifest -- we need to re-encrypt all fo the records, too. Force pushing.") + AppDependencies.jobManager.add(StorageForcePushJob()) + return Result.failure() + } + + val manifestWithNewVersion = currentManifest.copy(version = currentManifest.version + 1) + + return when (val result = repository.writeUnchangedManifest(storageServiceKey, manifestWithNewVersion)) { + StorageServiceRepository.WriteStorageRecordsResult.Success -> { + Log.i(TAG, "Successfully rotated the manifest. Clearing restore key.") + SignalStore.storageService.storageKeyForInitialDataRestore = null + Result.success() + } + StorageServiceRepository.WriteStorageRecordsResult.ConflictError -> { + Log.w(TAG, "Hit a conflict! Enqueuing a sync followed by another rotation.") + AppDependencies.jobManager.add(StorageSyncJob()) + AppDependencies.jobManager.add(StorageRotateManifestJob()) + Result.failure() + } + is StorageServiceRepository.WriteStorageRecordsResult.StatusCodeError -> { + Log.w(TAG, "Encountered a status code error during write, retrying.", result.exception) + Result.retry(defaultBackoff()) + } + + is StorageServiceRepository.WriteStorageRecordsResult.NetworkError -> { + Log.w(TAG, "Encountered a network error during write, retrying.", result.exception) + Result.retry(defaultBackoff()) + } + } + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): StorageRotateManifestJob { + return StorageRotateManifestJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt index c72fc97294..9ad64348b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.AccountRecordProcessor import org.thoughtcrime.securesms.storage.CallLinkRecordProcessor @@ -37,6 +38,9 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest import org.whispersystems.signalservice.api.storage.SignalStorageRecord import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord import org.whispersystems.signalservice.api.storage.StorageId +import org.whispersystems.signalservice.api.storage.StorageKey +import org.whispersystems.signalservice.api.storage.StorageServiceRepository +import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult import org.whispersystems.signalservice.api.storage.toSignalAccountRecord import org.whispersystems.signalservice.api.storage.toSignalCallLinkRecord import org.whispersystems.signalservice.api.storage.toSignalContactRecord @@ -163,8 +167,20 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param return } + val (storageServiceKey, usingTempKey) = SignalStore.storageService.storageKeyForInitialDataRestore?.let { + Log.i(TAG, "Using temporary storage key.") + it to true + } ?: run { + SignalStore.storageService.storageKey to false + } + try { - val needsMultiDeviceSync = performSync() + val needsMultiDeviceSync = performSync(storageServiceKey) + + if (usingTempKey) { + Log.i(TAG, "Used a temp key. Scheduling a job to rotate the manifest.") + AppDependencies.jobManager.add(StorageRotateManifestJob()) + } if (SignalStore.account.hasLinkedDevices && needsMultiDeviceSync) { AppDependencies.jobManager.add(MultiDeviceStorageSyncRequestJob()) @@ -196,15 +212,19 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param } @Throws(IOException::class, RetryLaterException::class, InvalidKeyException::class) - private fun performSync(): Boolean { + private fun performSync(storageServiceKey: StorageKey): Boolean { val stopwatch = Stopwatch("StorageSync") val db = SignalDatabase.rawDatabase - val accountManager = AppDependencies.signalServiceAccountManager - val storageServiceKey = SignalStore.storageService.storageKey + val repository = StorageServiceRepository(SignalNetwork.storageService) val localManifest = SignalStore.storageService.manifest - val remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifest.version).orElse(localManifest) - + val remoteManifest = when (val result = repository.getStorageManifestIfDifferentVersion(storageServiceKey, localManifest.version)) { + is ManifestIfDifferentVersionResult.DifferentVersion -> result.manifest + ManifestIfDifferentVersionResult.SameVersion -> localManifest + is ManifestIfDifferentVersionResult.DecryptionError -> throw result.exception + is ManifestIfDifferentVersionResult.NetworkError -> throw result.exception + is ManifestIfDifferentVersionResult.StatusCodeError -> throw result.exception + } stopwatch.split("remote-manifest") var self = freshSelf() @@ -248,7 +268,12 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param if (!idDifference.isEmpty) { Log.i(TAG, "[Remote Sync] Retrieving records for key difference.") - val remoteOnlyRecords = accountManager.readStorageRecords(storageServiceKey, idDifference.remoteOnlyIds) + val remoteOnlyRecords = when (val result = repository.readStorageRecords(storageServiceKey, remoteManifest.recordIkm, idDifference.remoteOnlyIds)) { + is StorageServiceRepository.StorageRecordResult.Success -> result.records + is StorageServiceRepository.StorageRecordResult.DecryptionError -> throw result.exception + is StorageServiceRepository.StorageRecordResult.NetworkError -> throw result.exception + is StorageServiceRepository.StorageRecordResult.StatusCodeError -> throw result.exception + } stopwatch.split("remote-records") @@ -292,6 +317,12 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param Log.i(TAG, "We are up-to-date with the remote storage state.") + if (remoteManifest.recordIkm == null && Recipient.self().storageServiceEncryptionV2Capability.isSupported) { + Log.w(TAG, "The SSRE2 capability is supported, but no recordIkm is set! Force pushing.") + AppDependencies.jobManager.add(StorageForcePushJob()) + return false + } + val remoteWriteOperation: WriteOperationResult = db.withinTransaction { self = freshSelf() @@ -308,9 +339,14 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param Log.i(TAG, "ID Difference :: $idDifference") WriteOperationResult( - SignalStorageManifest(remoteManifest.version + 1, SignalStore.account.deviceId, localStorageIds), - remoteInserts, - remoteDeletes + manifest = SignalStorageManifest( + version = remoteManifest.version + 1, + sourceDeviceId = SignalStore.account.deviceId, + recordIkm = remoteManifest.recordIkm, + storageIds = localStorageIds + ), + inserts = remoteInserts, + deletes = remoteDeletes ) } stopwatch.split("local-data-transaction") @@ -321,15 +357,19 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param StorageSyncValidations.validate(remoteWriteOperation, remoteManifest, needsForcePush, self) - val conflict = accountManager.writeStorageRecords(storageServiceKey, remoteWriteOperation.manifest, remoteWriteOperation.inserts, remoteWriteOperation.deletes) - - if (conflict.isPresent) { - Log.w(TAG, "Hit a conflict when trying to resolve the conflict! Retrying.") - throw RetryLaterException() + when (val result = repository.writeStorageRecords(storageServiceKey, remoteWriteOperation.manifest, remoteWriteOperation.inserts, remoteWriteOperation.deletes)) { + StorageServiceRepository.WriteStorageRecordsResult.Success -> Unit + is StorageServiceRepository.WriteStorageRecordsResult.StatusCodeError -> throw result.exception + is StorageServiceRepository.WriteStorageRecordsResult.NetworkError -> throw result.exception + StorageServiceRepository.WriteStorageRecordsResult.ConflictError -> { + Log.w(TAG, "Hit a conflict when trying to resolve the conflict! Retrying.") + throw RetryLaterException() + } } Log.i(TAG, "Saved new manifest. Now at version: ${remoteWriteOperation.manifest.versionString}") SignalStore.storageService.manifest = remoteWriteOperation.manifest + SignalStore.storageService.storageKeyForInitialDataRestore = null stopwatch.split("remote-write") @@ -344,7 +384,12 @@ class StorageSyncJob private constructor(parameters: Parameters) : BaseJob(param if (knownUnknownIds.isNotEmpty()) { Log.i(TAG, "We have ${knownUnknownIds.size} unknown records that we can now process.") - val remote = accountManager.readStorageRecords(storageServiceKey, knownUnknownIds) + val remote = when (val result = repository.readStorageRecords(storageServiceKey, remoteManifest.recordIkm, knownUnknownIds)) { + is StorageServiceRepository.StorageRecordResult.Success -> result.records + is StorageServiceRepository.StorageRecordResult.DecryptionError -> throw result.exception + is StorageServiceRepository.StorageRecordResult.NetworkError -> throw result.exception + is StorageServiceRepository.StorageRecordResult.StatusCodeError -> throw result.exception + } val records = StorageRecordCollection(remote) Log.i(TAG, "Found ${remote.size} of the known-unknowns remotely.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 98289b8602..eab99a490e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.api.AccountEntropyPool import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.ServiceIds @@ -30,6 +31,9 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents import org.whispersystems.signalservice.api.util.UuidUtil import org.whispersystems.signalservice.api.util.toByteArray import java.security.SecureRandom +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import org.signal.libsignal.messagebackup.AccountEntropyPool as LibSignalAccountEntropyPool class AccountValues internal constructor(store: KeyValueStore, context: Context) : SignalStoreValues(store) { @@ -79,6 +83,10 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) private const val KEY_IS_REGISTERED = "account.is_registered" private const val KEY_HAS_LINKED_DEVICES = "account.has_linked_devices" + + private const val KEY_ACCOUNT_ENTROPY_POOL = "account.account_entropy_pool" + + private val AEP_LOCK = ReentrantLock() } init { @@ -111,10 +119,37 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) KEY_PNI_IDENTITY_PRIVATE_KEY, KEY_USERNAME, KEY_USERNAME_LINK_ENTROPY, - KEY_USERNAME_LINK_SERVER_ID + KEY_USERNAME_LINK_SERVER_ID, + KEY_ACCOUNT_ENTROPY_POOL ) } + val accountEntropyPool: AccountEntropyPool + get() { + AEP_LOCK.withLock { + getString(KEY_ACCOUNT_ENTROPY_POOL, null)?.let { + return AccountEntropyPool(it) + } + + Log.i(TAG, "Generating Account Entropy Pool (AEP)...") + val newAep = LibSignalAccountEntropyPool.generate() + putString(KEY_ACCOUNT_ENTROPY_POOL, newAep) + return AccountEntropyPool(newAep) + } + } + + fun restoreAccountEntropyPool(aep: AccountEntropyPool) { + AEP_LOCK.withLock { + store.beginWrite().putString(KEY_ACCOUNT_ENTROPY_POOL, aep.value).commit() + } + } + + fun resetAccountEntropyPool() { + AEP_LOCK.withLock { + store.beginWrite().putString(KEY_ACCOUNT_ENTROPY_POOL, null).commit() + } + } + /** The local user's [ACI]. */ val aci: ACI? get() = ACI.parseOrNull(getString(KEY_ACI, null)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 4915219d34..75ab10bf35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -101,7 +101,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { * Key used to backup messages. */ val messageBackupKey: MessageBackupKey - get() = SignalStore.svr.masterKey.derivateMessageBackupKey() + get() = SignalStore.account.accountEntropyPool.deriveMessageBackupKey() /** * Key used to backup media. Purely random and separate from the message backup key. diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.kt index c57a6c595a..9cb485d2a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.kt @@ -1,22 +1,27 @@ package org.thoughtcrime.securesms.keyvalue +import org.signal.core.util.logging.Log import org.whispersystems.signalservice.api.storage.SignalStorageManifest import org.whispersystems.signalservice.api.storage.StorageKey import org.whispersystems.signalservice.api.util.Preconditions class StorageServiceValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) { companion object { + private val TAG = Log.tag(StorageServiceValues::class) + private const val LAST_SYNC_TIME = "storage.last_sync_time" private const val NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore" private const val MANIFEST = "storage.manifest" + + // TODO [linked-device] No need to track this separately -- we'd get the AEP from the primary private const val SYNC_STORAGE_KEY = "storage.syncStorageKey" + private const val INITIAL_RESTORE_STORAGE_KEY = "storage.initialRestoreStorageKey" } public override fun onFirstEverAppLaunch() = Unit public override fun getKeysToIncludeInBackup(): List = emptyList() - @get:Synchronized val storageKey: StorageKey get() { if (store.containsKey(SYNC_STORAGE_KEY)) { @@ -54,4 +59,30 @@ class StorageServiceValues internal constructor(store: KeyValueStore) : SignalSt set(manifest) { putBlob(MANIFEST, manifest.serialize()) } + + /** + * The [StorageKey] that should be used for our initial storage service data restore. + * The presence of this value indicates that it hasn't been used yet. + * Once there has been *any* write to storage service, this value needs to be cleared. + */ + @get:Synchronized + @set:Synchronized + var storageKeyForInitialDataRestore: StorageKey? + get() { + return getBlob(INITIAL_RESTORE_STORAGE_KEY, null)?.let { StorageKey(it) } + } + set(value) { + if (value != storageKeyForInitialDataRestore) { + if (value == storageKey) { + Log.w(TAG, "The key already matches the one derived from the AEP! All good, no need to store it.") + store.beginWrite().putBlob(INITIAL_RESTORE_STORAGE_KEY, null).commit() + } else if (value != null) { + Log.w(TAG, "Setting initial restore key!", Throwable()) + store.beginWrite().putBlob(INITIAL_RESTORE_STORAGE_KEY, value.serialize()).commit() + } else { + Log.w(TAG, "Clearing initial restore key!", Throwable()) + store.beginWrite().putBlob(INITIAL_RESTORE_STORAGE_KEY, null).commit() + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt index 788a26dd8d..6a517d9658 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.kt @@ -2,12 +2,8 @@ package org.thoughtcrime.securesms.keyvalue import org.signal.core.util.StringStringSerializer import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.util.JsonUtils import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.kbs.PinHashUtil.localPinHash -import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse -import java.io.IOException -import java.security.SecureRandom class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) { companion object { @@ -16,8 +12,6 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s const val REGISTRATION_LOCK_ENABLED: String = "kbs.v2_lock_enabled" const val OPTED_OUT: String = "kbs.opted_out" - private const val MASTER_KEY = "kbs.registration_lock_master_key" - private const val TOKEN_RESPONSE = "kbs.token_response" private const val PIN = "kbs.pin" private const val LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash" private const val LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp" @@ -42,7 +36,6 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s fun clearRegistrationLockAndPin() { store.beginWrite() .remove(REGISTRATION_LOCK_ENABLED) - .remove(TOKEN_RESPONSE) .remove(LOCK_LOCAL_PIN_HASH) .remove(PIN) .remove(LAST_CREATE_FAILED_TIMESTAMP) @@ -52,10 +45,11 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s .commit() } + @Deprecated("Switch to restoring AEP instead") @Synchronized fun setMasterKey(masterKey: MasterKey, pin: String?) { store.beginWrite().apply { - putBlob(MASTER_KEY, masterKey.serialize()) +// putBlob(MASTER_KEY, masterKey.serialize()) putLong(LAST_CREATE_FAILED_TIMESTAMP, -1) putBoolean(OPTED_OUT, false) @@ -71,10 +65,21 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s }.commit() } + @Synchronized + fun setPin(pin: String) { + store.beginWrite() + .putString(PIN, pin) + .putString(LOCK_LOCAL_PIN_HASH, localPinHash(pin)) + .commit() + } + @Synchronized fun setPinIfNotPresent(pin: String) { if (store.getString(PIN, null) == null) { - store.beginWrite().putString(PIN, pin).commit() + store.beginWrite() + .putString(PIN, pin) + .putString(LOCK_LOCAL_PIN_HASH, localPinHash(pin)) + .commit() } } @@ -94,33 +99,18 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s return getLong(LAST_CREATE_FAILED_TIMESTAMP, -1) > 0 } - /** Returns the Master Key, lazily creating one if needed. */ - @get:Synchronized + /** Returns the Master Key */ val masterKey: MasterKey - get() { - val blob = store.getBlob(MASTER_KEY, null) - if (blob != null) { - return MasterKey(blob) - } - - Log.i(TAG, "Generating Master Key...", Throwable()) - val masterKey = MasterKey.createNew(SecureRandom()) - store.beginWrite().putBlob(MASTER_KEY, masterKey.serialize()).commit() - return masterKey - } + get() = SignalStore.account.accountEntropyPool.deriveMasterKey() @get:Synchronized val pinBackedMasterKey: MasterKey? /** Returns null if master key is not backed up by a pin. */ get() { if (!isRegistrationLockEnabled) return null - return rawMasterKey + return masterKey } - @get:Synchronized - private val rawMasterKey: MasterKey? - get() = getBlob(MASTER_KEY, null)?.let { MasterKey(it) } - @get:Synchronized val registrationLockToken: String? get() { @@ -131,8 +121,7 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s @get:Synchronized val recoveryPassword: String? get() { - val masterKey = rawMasterKey - return if (masterKey != null && hasOptedInWithAccess()) { + return if (hasOptedInWithAccess()) { masterKey.deriveRegistrationRecoveryPassword() } else { null @@ -242,8 +231,6 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s fun optOut() { store.beginWrite() .putBoolean(OPTED_OUT, true) - .remove(TOKEN_RESPONSE) - .putBlob(MASTER_KEY, MasterKey.createNew(SecureRandom()).serialize()) .remove(LOCK_LOCAL_PIN_HASH) .remove(PIN) .remove(RESTORED_VIA_ACCOUNT_ENTROPY_KEY) @@ -256,17 +243,5 @@ class SvrValues internal constructor(store: KeyValueStore) : SignalStoreValues(s return getBoolean(OPTED_OUT, false) } - @get:Synchronized - val registrationLockTokenResponse: TokenResponse? - get() { - val token = store.getString(TOKEN_RESPONSE, null) ?: return null - - try { - return JsonUtils.fromJson(token, TokenResponse::class.java) - } catch (e: IOException) { - throw AssertionError(e) - } - } - var lastRefreshAuthTimestamp: Long by longValue(SVR_LAST_AUTH_REFRESH_TIMESTAMP, 0L) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java index f6c70ee30b..5c8d3e1b90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.RemoteConfig; import org.whispersystems.signalservice.api.account.AccountAttributes; public final class LogSectionCapabilities implements LogSection { @@ -30,17 +31,20 @@ public final class LogSectionCapabilities implements LogSection { Recipient self = Recipient.self(); - AccountAttributes.Capabilities localCapabilities = AppCapabilities.getCapabilities(false); + AccountAttributes.Capabilities localCapabilities = AppCapabilities.getCapabilities(false, RemoteConfig.getStorageServiceEncryptionV2()); RecipientRecord.Capabilities globalCapabilities = SignalDatabase.recipients().getCapabilities(self.getId()); StringBuilder builder = new StringBuilder().append("-- Local").append("\n") .append("DeleteSync: ").append(localCapabilities.getDeleteSync()).append("\n") .append("VersionedExpirationTimer: ").append(localCapabilities.getVersionedExpirationTimer()).append("\n") + .append("StorageServiceEncryptionV2: ").append(localCapabilities.getStorageServiceEncryptionV2()).append("\n") .append("\n") .append("-- Global").append("\n"); if (globalCapabilities != null) { builder.append("DeleteSync: ").append(globalCapabilities.getDeleteSync()).append("\n"); + builder.append("VersionedExpirationTimer: ").append(globalCapabilities.getVersionedExpirationTimer()).append("\n"); + builder.append("StorageServiceEncryptionV2: ").append(globalCapabilities.getStorageServiceEncryptionV2()).append("\n"); builder.append("\n"); } else { builder.append("Self not found!"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/AepMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/AepMigrationJob.kt new file mode 100644 index 0000000000..3ffcbb570d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/AepMigrationJob.kt @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.migrations + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobs.StorageForcePushJob +import org.thoughtcrime.securesms.jobs.Svr2MirrorJob + +/** + * Migration for when we introduce the Account Entropy Pool (AEP). + */ +internal class AepMigrationJob( + parameters: Parameters = Parameters.Builder().build() +) : MigrationJob(parameters) { + + companion object { + val TAG = Log.tag(AepMigrationJob::class.java) + const val KEY = "AepMigrationJob" + } + + override fun getFactoryKey(): String = KEY + + override fun isUiBlocking(): Boolean = false + + override fun performMigration() { + AppDependencies.jobManager.add(Svr2MirrorJob()) + AppDependencies.jobManager.add(StorageForcePushJob()) + } + + override fun shouldRetry(e: Exception): Boolean = false + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): AepMigrationJob { + return AepMigrationJob(parameters) + } + } +} 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 c74ad49ca5..f646a448fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -160,9 +160,10 @@ public class ApplicationMigrations { static final int BACKFILL_DIGESTS_V3 = 116; static final int SVR2_ENCLAVE_UPDATE_2 = 117; static final int WALLPAPER_MIGRATION_CLEANUP = 118; + static final int AEP_INTRODUCTION = 119; } - public static final int CURRENT_VERSION = 118; + public static final int CURRENT_VERSION = 119; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -733,6 +734,10 @@ public class ApplicationMigrations { jobs.put(Version.WALLPAPER_MIGRATION_CLEANUP, new WallpaperCleanupMigrationJob()); } + if (lastSeenVersion < Version.AEP_INTRODUCTION) { + jobs.put(Version.AEP_INTRODUCTION, new AepMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt b/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt index 0ebe5cfd83..e09a0bb1f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt @@ -10,6 +10,7 @@ import org.whispersystems.signalservice.api.archive.ArchiveApi import org.whispersystems.signalservice.api.attachment.AttachmentApi import org.whispersystems.signalservice.api.keys.KeysApi import org.whispersystems.signalservice.api.link.LinkDeviceApi +import org.whispersystems.signalservice.api.storage.StorageServiceApi /** * A convenient way to access network operations, similar to [org.thoughtcrime.securesms.database.SignalDatabase] and [org.thoughtcrime.securesms.keyvalue.SignalStore]. @@ -26,4 +27,7 @@ object SignalNetwork { val linkDevice: LinkDeviceApi get() = AppDependencies.linkDeviceApi + + val storageService: StorageServiceApi + get() = AppDependencies.storageServiceApi } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt index f936490261..a850469c59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt @@ -168,7 +168,8 @@ object SvrRepository { SignalStore.registration.localRegistrationMetadata = metadata.copy(masterKey = response.masterKey.serialize().toByteString(), pin = userPin) } - SignalStore.svr.setMasterKey(response.masterKey, userPin) + SignalStore.storageService.storageKeyForInitialDataRestore = response.masterKey.deriveStorageServiceKey() + SignalStore.svr.setPin(userPin) SignalStore.svr.isRegistrationLockEnabled = false SignalStore.pin.resetPinReminders() SignalStore.pin.keyboardType = pinKeyboardType @@ -267,7 +268,7 @@ object SvrRepository { if (overallResponse is BackupResponse.Success) { Log.i(TAG, "[setPin] Success!", true) - SignalStore.svr.setMasterKey(masterKey, userPin) + SignalStore.svr.setPin(userPin) responses .filterIsInstance() .forEach { @@ -320,13 +321,14 @@ object SvrRepository { Log.i(TAG, "[onRegistrationComplete] ReRegistration Skip SMS", true) } - SignalStore.svr.setMasterKey(masterKey, userPin) + SignalStore.storageService.storageKeyForInitialDataRestore = masterKey.deriveStorageServiceKey() + SignalStore.svr.setPin(userPin) SignalStore.pin.resetPinReminders() AppDependencies.jobManager.add(ResetSvrGuessCountJob()) } else if (masterKey != null) { Log.i(TAG, "[onRegistrationComplete] ReRegistered with key without pin") - SignalStore.svr.setMasterKey(masterKey, null) + SignalStore.storageService.storageKeyForInitialDataRestore = masterKey.deriveStorageServiceKey() } else if (hasPinToRestore) { Log.i(TAG, "[onRegistrationComplete] Has a PIN to restore.", true) SignalStore.svr.clearRegistrationLockAndPin() diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index b6bc37f98d..55f9bd2062 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -321,6 +321,9 @@ class Recipient( /** The user's capability to handle tracking an expire timer version. */ val versionedExpirationTimerCapability: Capability = capabilities.versionedExpirationTimer + /** The user's capability to handle the new storage record encryption scheme. */ + val storageServiceEncryptionV2Capability: Capability = capabilities.storageServiceEncryptionV2 + /** The state around whether we can send sealed sender to this user. */ val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) { SealedSenderAccessMode.DISABLED diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt index 7f95edd8d6..dae2a1adb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt @@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.service.DirectoryRefreshListener import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SvrNoDataException @@ -274,7 +275,8 @@ object RegistrationRepository { withContext(Dispatchers.IO) { val credentialSet = SvrAuthCredentialSet(svr2Credentials = svr2Credentials, svr3Credentials = svr3Credentials) val masterKey = SvrRepository.restoreMasterKeyPreRegistration(credentialSet, pin) - SignalStore.svr.setMasterKey(masterKey, pin) + SignalStore.storageService.storageKeyForInitialDataRestore = masterKey.deriveStorageServiceKey() + SignalStore.svr.setPin(pin) return@withContext masterKey } @@ -420,7 +422,7 @@ object RegistrationRepository { registrationLock = registrationLock, unidentifiedAccessKey = unidentifiedAccessKey, unrestrictedUnidentifiedAccess = universalUnidentifiedAccess, - capabilities = AppCapabilities.getCapabilities(true), + capabilities = AppCapabilities.getCapabilities(true, RemoteConfig.storageServiceEncryptionV2), discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE, name = null, pniRegistrationId = registrationData.pniRegistrationId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt index 915147dc8b..4de0b2fbdc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt @@ -81,13 +81,13 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } private fun createChallengeRequiredProcessor(errorResult: NetworkResult.StatusCodeError): VerificationCodeRequestResult { - if (errorResult.body == null) { + if (errorResult.stringBody == null) { Log.w(TAG, "Attempted to parse error body with response code ${errorResult.code} for list of requested information, but body was null.") return UnknownError(errorResult.exception) } try { - val response = JsonUtil.fromJson(errorResult.body, RegistrationSessionMetadataJson::class.java) + val response = JsonUtil.fromJson(errorResult.stringBody, RegistrationSessionMetadataJson::class.java) return ChallengeRequired(Challenge.parse(response.requestedInformation)) } catch (parseException: IOException) { Log.w(TAG, "Attempted to parse error body for list of requested information, but encountered exception.", parseException) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt index c572883b2a..3c4a45baf8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt @@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.registration.fcm.PushChallengeRequest import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.service.DirectoryRefreshListener import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SvrNoDataException @@ -414,7 +415,7 @@ object RegistrationRepository { registrationLock = registrationLock, unidentifiedAccessKey = unidentifiedAccessKey, unrestrictedUnidentifiedAccess = universalUnidentifiedAccess, - capabilities = AppCapabilities.getCapabilities(true), + capabilities = AppCapabilities.getCapabilities(true, RemoteConfig.storageServiceEncryptionV2), discoverableByPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE, name = null, pniRegistrationId = registrationData.pniRegistrationId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 0dc6e41e17..eca4900c2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1143,5 +1143,13 @@ object RemoteConfig { hotSwappable = true ) + /** Whether or not this device supports the new storage service recordIkm encryption. */ + @JvmStatic + val storageServiceEncryptionV2: Boolean by remoteBoolean( + key = "android.ssre2", + defaultValue = false, + hotSwappable = true + ) + // endregion } diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt b/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt index dca0b8a736..5d1f3bb0de 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt @@ -3,8 +3,9 @@ package org.thoughtcrime.securesms import org.signal.core.util.Base64 import org.signal.spinner.Plugin import org.signal.spinner.PluginResult -import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork +import org.whispersystems.signalservice.api.storage.StorageServiceRepository class StorageServicePlugin : Plugin { override val name: String = "Storage" @@ -14,11 +15,18 @@ class StorageServicePlugin : Plugin { val columns = listOf("Type", "Id", "Data") val rows = mutableListOf>() - val manager = AppDependencies.signalServiceAccountManager + val repository = StorageServiceRepository(SignalNetwork.storageService) val storageServiceKey = SignalStore.storageService.storageKey - val storageManifestVersion = manager.storageManifestVersion - val manifest = manager.getStorageManifestIfDifferentVersion(storageServiceKey, storageManifestVersion - 1).get() - val signalStorageRecords = manager.readStorageRecords(storageServiceKey, manifest.storageIds) + + val manifest = when (val result = repository.getStorageManifest(storageServiceKey)) { + is StorageServiceRepository.ManifestResult.Success -> result.manifest + else -> return PluginResult.StringResult("Failed to find manifest!") + } + + val signalStorageRecords = when (val result = repository.readStorageRecords(storageServiceKey, manifest.recordIkm, manifest.storageIds)) { + is StorageServiceRepository.StorageRecordResult.Success -> result.records + else -> return PluginResult.StringResult("Failed to read records!") + } for (record in signalStorageRecords) { val row = mutableListOf() diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index e1c09fba59..d2a381643a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -127,7 +127,8 @@ object RecipientDatabaseTestUtils { capabilities = RecipientRecord.Capabilities( rawBits = capabilities, deleteSync = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.DELETE_SYNC, RecipientTable.Capabilities.BIT_LENGTH).toInt()), - versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.VERSIONED_EXPIRATION_TIMER, RecipientTable.Capabilities.BIT_LENGTH).toInt()) + versionedExpirationTimer = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.VERSIONED_EXPIRATION_TIMER, RecipientTable.Capabilities.BIT_LENGTH).toInt()), + storageServiceEncryptionV2 = Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientTable.Capabilities.STORAGE_SERVICE_ENCRYPTION_V2, RecipientTable.Capabilities.BIT_LENGTH).toInt()) ), storageId = storageId, mentionSetting = mentionSetting, diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt index e758a983e4..f800ac79e5 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt @@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.registration.RegistrationApi import org.whispersystems.signalservice.api.services.CallLinksService import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.ProfileService +import org.whispersystems.signalservice.api.storage.StorageServiceApi import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.push.PushServiceSocket import java.util.function.Supplier @@ -227,4 +228,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider { override fun provideRegistrationApi(pushServiceSocket: PushServiceSocket): RegistrationApi { return mockk() } + + override fun provideStorageServiceApi(pushServiceSocket: PushServiceSocket): StorageServiceApi { + return mockk() + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt new file mode 100644 index 0000000000..dc20cbbddd --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/AccountEntropyPool.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api + +import org.whispersystems.signalservice.api.backup.MessageBackupKey +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.signal.libsignal.messagebackup.AccountEntropyPool as LibSignalAccountEntropyPool + +/** + * The Root of All Entropy. You can use this to derive the [MasterKey] or [MessageBackupKey]. + */ +class AccountEntropyPool(val value: String) { + + companion object { + fun generate(): AccountEntropyPool { + return AccountEntropyPool(LibSignalAccountEntropyPool.generate()) + } + } + + fun deriveMasterKey(): MasterKey { + return MasterKey(LibSignalAccountEntropyPool.deriveSvrKey(value)) + } + + fun deriveMessageBackupKey(): MessageBackupKey { + val libSignalBackupKey = LibSignalAccountEntropyPool.deriveBackupKey(value) + return MessageBackupKey(libSignalBackupKey.serialize()) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt index ad05e1d07d..76342e1ad0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt @@ -105,8 +105,8 @@ sealed class NetworkResult( data class NetworkError(val exception: IOException) : NetworkResult() /** Indicates we got a response, but it was a non-2xx response. */ - data class StatusCodeError(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult() { - constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.body, e) + data class StatusCodeError(val code: Int, val stringBody: String?, val binaryBody: ByteArray?, val exception: NonSuccessfulResponseCodeException) : NetworkResult() { + constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.stringBody, e.binaryBody, e) } /** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */ @@ -143,6 +143,8 @@ sealed class NetworkResult( * If it's non-successful, [transform] lambda is not run, and instead the original failure will be propagated. * Useful for changing the type of a result. * + * If an exception is thrown during [transform], this is mapped to an [ApplicationError]. + * * ```kotlin * val user: NetworkResult = NetworkResult * .fromFetch { fetchRemoteUserModel() } @@ -151,10 +153,16 @@ sealed class NetworkResult( */ fun map(transform: (T) -> R): NetworkResult { return when (this) { - is Success -> Success(transform(this.result)).runOnStatusCodeError(statusCodeErrorActions) + is Success -> { + try { + Success(transform(this.result)).runOnStatusCodeError(statusCodeErrorActions) + } catch (e: Throwable) { + ApplicationError(e).runOnStatusCodeError(statusCodeErrorActions) + } + } is NetworkError -> NetworkError(exception).runOnStatusCodeError(statusCodeErrorActions) is ApplicationError -> ApplicationError(throwable).runOnStatusCodeError(statusCodeErrorActions) - is StatusCodeError -> StatusCodeError(code, body, exception).runOnStatusCodeError(statusCodeErrorActions) + is StatusCodeError -> StatusCodeError(code, stringBody, binaryBody, exception).runOnStatusCodeError(statusCodeErrorActions) } } @@ -204,7 +212,7 @@ sealed class NetworkResult( is Success -> result(this.result).runOnStatusCodeError(statusCodeErrorActions) is NetworkError -> NetworkError(exception).runOnStatusCodeError(statusCodeErrorActions) is ApplicationError -> ApplicationError(throwable).runOnStatusCodeError(statusCodeErrorActions) - is StatusCodeError -> StatusCodeError(code, body, exception).runOnStatusCodeError(statusCodeErrorActions) + is StatusCodeError -> StatusCodeError(code, stringBody, binaryBody, exception).runOnStatusCodeError(statusCodeErrorActions) } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index e5aa08c6a6..3cb1b3ecd9 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -6,14 +6,11 @@ package org.whispersystems.signalservice.api; -import com.squareup.wire.FieldEncoding; - import org.signal.core.util.Base64; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; -import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; import org.signal.libsignal.usernames.Username.UsernameLink; @@ -39,19 +36,10 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.UsernameLinkComponents; -import org.whispersystems.signalservice.api.push.exceptions.NoContentException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; -import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.CdsiV2Service; -import org.whispersystems.signalservice.api.storage.SignalStorageCipher; -import org.whispersystems.signalservice.api.storage.SignalStorageManifest; -import org.whispersystems.signalservice.api.storage.SignalStorageModels; -import org.whispersystems.signalservice.api.storage.SignalStorageRecord; -import org.whispersystems.signalservice.api.storage.StorageId; -import org.whispersystems.signalservice.api.storage.StorageKey; -import org.whispersystems.signalservice.api.storage.StorageManifestKey; import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2; import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3; import org.whispersystems.signalservice.api.util.CredentialsProvider; @@ -71,19 +59,11 @@ import org.whispersystems.signalservice.internal.push.RemoteConfigResponse; import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse; import org.whispersystems.signalservice.internal.push.WhoAmIResponse; import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory; -import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; -import org.whispersystems.signalservice.internal.storage.protos.ReadOperation; -import org.whispersystems.signalservice.internal.storage.protos.StorageItem; -import org.whispersystems.signalservice.internal.storage.protos.StorageItems; -import org.whispersystems.signalservice.internal.storage.protos.StorageManifest; -import org.whispersystems.signalservice.internal.storage.protos.WriteOperation; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -94,7 +74,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; -import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -111,8 +90,6 @@ public class SignalServiceAccountManager { private static final String TAG = SignalServiceAccountManager.class.getSimpleName(); - private static final int STORAGE_READ_MAX_ITEMS = 1000; - private final PushServiceSocket pushServiceSocket; private final CredentialsProvider credentials; private final GroupsV2Operations groupsV2Operations; @@ -290,119 +267,6 @@ public class SignalServiceAccountManager { } } - - public Optional getStorageManifest(StorageKey storageKey) throws IOException { - try { - String authToken = this.pushServiceSocket.getStorageAuth(); - StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken); - - return Optional.of(SignalStorageModels.remoteToLocalStorageManifest(storageManifest, storageKey)); - } catch (InvalidKeyException | NotFoundException e) { - Log.w(TAG, "Error while fetching manifest.", e); - return Optional.empty(); - } - } - - public long getStorageManifestVersion() throws IOException { - try { - String authToken = this.pushServiceSocket.getStorageAuth(); - StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken); - - return storageManifest.version; - } catch (NotFoundException e) { - return 0; - } - } - - public Optional getStorageManifestIfDifferentVersion(StorageKey storageKey, long manifestVersion) throws IOException, InvalidKeyException { - try { - String authToken = this.pushServiceSocket.getStorageAuth(); - StorageManifest storageManifest = this.pushServiceSocket.getStorageManifestIfDifferentVersion(authToken, manifestVersion); - - if (storageManifest.value_.size() == 0) { - Log.w(TAG, "Got an empty storage manifest!"); - return Optional.empty(); - } - - return Optional.of(SignalStorageModels.remoteToLocalStorageManifest(storageManifest, storageKey)); - } catch (NoContentException e) { - return Optional.empty(); - } - } - - public List readStorageRecords(StorageKey storageKey, List storageKeys) throws IOException, InvalidKeyException { - if (storageKeys.isEmpty()) { - return Collections.emptyList(); - } - - List result = new ArrayList<>(); - Map typeMap = new HashMap<>(); - List readOperations = new LinkedList<>(); - List readKeys = new LinkedList<>(); - - for (StorageId key : storageKeys) { - typeMap.put(ByteString.of(key.getRaw()), key.getType()); - - if (readKeys.size() >= STORAGE_READ_MAX_ITEMS) { - Log.i(TAG, "Going over max read items. Starting a new read operation."); - readOperations.add(new ReadOperation.Builder().readKey(readKeys).build()); - readKeys = new LinkedList<>(); - } - - if (StorageId.isKnownType(key.getType())) { - readKeys.add(ByteString.of(key.getRaw())); - } else { - result.add(SignalStorageRecord.forUnknown(key)); - } - } - - if (readKeys.size() > 0) { - readOperations.add(new ReadOperation.Builder().readKey(readKeys).build()); - } - - Log.i(TAG, "Reading " + storageKeys.size() + " items split over " + readOperations.size() + " page(s)."); - - String authToken = this.pushServiceSocket.getStorageAuth(); - - for (ReadOperation readOperation : readOperations) { - StorageItems items = this.pushServiceSocket.readStorageItems(authToken, readOperation); - - for (StorageItem item : items.items) { - Integer type = typeMap.get(item.key); - if (type != null) { - result.add(SignalStorageModels.remoteToLocalStorageRecord(item, type, storageKey)); - } else { - Log.w(TAG, "No type found! Skipping."); - } - } - } - - return result; - } - /** - * @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent. - */ - public Optional resetStorageRecords(StorageKey storageKey, - SignalStorageManifest manifest, - List allRecords) - throws IOException, InvalidKeyException - { - return writeStorageRecords(storageKey, manifest, allRecords, Collections.emptyList(), true); - } - - /** - * @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent. - */ - public Optional writeStorageRecords(StorageKey storageKey, - SignalStorageManifest manifest, - List inserts, - List deletes) - throws IOException, InvalidKeyException - { - return writeStorageRecords(storageKey, manifest, inserts, deletes, false); - } - - /** * Enables registration lock for this account. */ @@ -417,83 +281,6 @@ public class SignalServiceAccountManager { pushServiceSocket.disableRegistrationLockV2(); } - /** - * @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent. - */ - private Optional writeStorageRecords(StorageKey storageKey, - SignalStorageManifest manifest, - List inserts, - List deletes, - boolean clearAll) - throws IOException, InvalidKeyException - { - ManifestRecord.Builder manifestRecordBuilder = new ManifestRecord.Builder() - .sourceDevice(manifest.sourceDeviceId) - .version(manifest.version); - - - manifestRecordBuilder.identifiers( - manifest.storageIds.stream() - .map(id -> { - ManifestRecord.Identifier.Builder builder = new ManifestRecord.Identifier.Builder() - .raw(ByteString.of(id.getRaw())); - if (!id.isUnknown()) { - builder.type(ManifestRecord.Identifier.Type.Companion.fromValue(id.getType())); - } else { - builder.type(ManifestRecord.Identifier.Type.UNKNOWN); - builder.addUnknownField(2, FieldEncoding.VARINT, id.getType()); - } - return builder.build(); - }) - .collect(Collectors.toList()) - ); - - String authToken = this.pushServiceSocket.getStorageAuth(); - StorageManifestKey manifestKey = storageKey.deriveManifestKey(manifest.version); - byte[] encryptedRecord = SignalStorageCipher.encrypt(manifestKey, manifestRecordBuilder.build().encode()); - StorageManifest storageManifest = new StorageManifest.Builder() - .version(manifest.version) - .value_(ByteString.of(encryptedRecord)) - .build(); - - WriteOperation.Builder writeBuilder = new WriteOperation.Builder().manifest(storageManifest); - - writeBuilder.insertItem( - inserts.stream() - .map(insert -> SignalStorageModels.localToRemoteStorageRecord(insert, storageKey)) - .collect(Collectors.toList()) - ); - - if (clearAll) { - writeBuilder.clearAll(true); - } else { - writeBuilder.deleteKey( - deletes.stream() - .map(delete -> ByteString.of(delete)) - .collect(Collectors.toList()) - ); - } - - Optional conflict = this.pushServiceSocket.writeStorageContacts(authToken, writeBuilder.build()); - - if (conflict.isPresent()) { - StorageManifestKey conflictKey = storageKey.deriveManifestKey(conflict.get().version); - byte[] rawManifestRecord = SignalStorageCipher.decrypt(conflictKey, conflict.get().value_.toByteArray()); - ManifestRecord record = ManifestRecord.ADAPTER.decode(rawManifestRecord); - List ids = new ArrayList<>(record.identifiers.size()); - - for (ManifestRecord.Identifier id : record.identifiers) { - ids.add(StorageId.forType(id.raw.toByteArray(), id.type.getValue())); - } - - SignalStorageManifest conflictManifest = new SignalStorageManifest(record.version, record.sourceDevice, ids); - - return Optional.of(conflictManifest); - } else { - return Optional.empty(); - } - } - public RemoteConfigResult getRemoteConfig() throws IOException { RemoteConfigResponse response = this.pushServiceSocket.getRemoteConfig(); Map out = new HashMap<>(); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index b31ac74f77..bc04bc50be 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -1657,16 +1657,20 @@ public class SignalServiceMessageSender { SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.Keys.Builder builder = new SyncMessage.Keys.Builder(); - if (keysMessage.getStorageService().isPresent()) { - builder.storageService(ByteString.of(keysMessage.getStorageService().get().serialize())); + if (keysMessage.getStorageService() != null) { + builder.storageService(ByteString.of(keysMessage.getStorageService().serialize())); } - if (keysMessage.getMaster().isPresent()) { - builder.master(ByteString.of(keysMessage.getMaster().get().serialize())); + if (keysMessage.getMaster() != null) { + builder.master(ByteString.of(keysMessage.getMaster().serialize())); } - if (builder.storageService == null && builder.master == null) { - Log.w(TAG, "Invalid keys message!"); + if (keysMessage.getAccountEntropyPool() != null) { + builder.accountEntropyPool(keysMessage.getAccountEntropyPool().getValue()); + } + + if (keysMessage.getMediaRootBackupKey() != null) { + builder.mediaRootBackupKey(ByteString.of(keysMessage.getMediaRootBackupKey().getValue())); } return container.syncMessage(syncMessage.keys(builder.build()).build()).build(); @@ -2689,7 +2693,7 @@ public class SignalServiceMessageSender { return socket.getPreKeys(recipient, sealedSenderAccess, deviceId); } catch (NonSuccessfulResponseCodeException e) { - if (e.getCode() == 401 && story) { + if (e.code == 401 && story) { Log.d(TAG, "Got 401 when fetching prekey for story. Trying without UD."); return socket.getPreKeys(recipient, null, deviceId); } else { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt index b578e56753..31a2b97cc8 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.kt @@ -56,6 +56,7 @@ class AccountAttributes @JsonCreator constructor( data class Capabilities @JsonCreator constructor( @JsonProperty val storage: Boolean, @JsonProperty val deleteSync: Boolean, - @JsonProperty val versionedExpirationTimer: Boolean + @JsonProperty val versionedExpirationTimer: Boolean, + @JsonProperty("ssre2") val storageServiceEncryptionV2: Boolean ) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/Crypto.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/Crypto.kt new file mode 100644 index 0000000000..f65df5dbba --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/Crypto.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.crypto + +import org.signal.libsignal.protocol.kdf.HKDF + +/** + * A collection of cryptographic functions in the same namespace for easy access. + */ +object Crypto { + + fun hkdf(inputKeyMaterial: ByteArray, info: ByteArray, outputLength: Int, salt: ByteArray? = null): ByteArray { + return HKDF.deriveSecrets(inputKeyMaterial, salt, info, outputLength) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java index 950b645578..e3ab7ddaf7 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java @@ -46,7 +46,7 @@ public final class MasterKey { return derive("Logging Key"); } - public MessageBackupKey derivateMessageBackupKey() { + public MessageBackupKey deriveMessageBackupKey() { // TODO [backup] Derive from AEP return new MessageBackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32)); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/KeysMessage.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/KeysMessage.java deleted file mode 100644 index d9e9d72751..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/KeysMessage.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.whispersystems.signalservice.api.messages.multidevice; - - -import org.whispersystems.signalservice.api.kbs.MasterKey; -import org.whispersystems.signalservice.api.storage.StorageKey; - -import java.util.Optional; - -public class KeysMessage { - - private final Optional storageService; - private final Optional master; - - public KeysMessage(Optional storageService, Optional master) { - this.storageService = storageService; - this.master = master; - } - - public Optional getStorageService() { - return storageService; - } - - public Optional getMaster() { - return master; - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/KeysMessage.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/KeysMessage.kt new file mode 100644 index 0000000000..7ffe036f33 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/KeysMessage.kt @@ -0,0 +1,13 @@ +package org.whispersystems.signalservice.api.messages.multidevice + +import org.whispersystems.signalservice.api.AccountEntropyPool +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.storage.StorageKey + +data class KeysMessage( + val storageService: StorageKey?, + val master: MasterKey?, + val accountEntropyPool: AccountEntropyPool?, + val mediaRootBackupKey: MediaRootBackupKey? +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index 606250c99f..2b689e26eb 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -198,13 +198,17 @@ public class SignalServiceProfile { @JsonProperty private boolean versionedExpirationTimer; + @JsonProperty("ssre2") + private boolean storageServiceEncryptionV2; + @JsonCreator public Capabilities() {} - public Capabilities(boolean storage, boolean deleteSync, boolean versionedExpirationTimer) { - this.storage = storage; - this.deleteSync = deleteSync; - this.versionedExpirationTimer = versionedExpirationTimer; + public Capabilities(boolean storage, boolean deleteSync, boolean versionedExpirationTimer, boolean storageServiceEncryptionV2) { + this.storage = storage; + this.deleteSync = deleteSync; + this.versionedExpirationTimer = versionedExpirationTimer; + this.storageServiceEncryptionV2 = storageServiceEncryptionV2; } public boolean isStorage() { @@ -218,6 +222,10 @@ public class SignalServiceProfile { public boolean isVersionedExpirationTimer() { return versionedExpirationTimer; } + + public boolean isStorageServiceEncryptionV2() { + return storageServiceEncryptionV2; + } } public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java deleted file mode 100644 index c6c7090f60..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.whispersystems.signalservice.api.push.exceptions; - -import java.io.IOException; - -/** - * Indicates a server response that is not successful, typically something outside the 2xx range. - */ -public class NonSuccessfulResponseCodeException extends IOException { - - private final int code; - private final String body; - - public NonSuccessfulResponseCodeException(int code) { - super("StatusCode: " + code); - this.code = code; - this.body = null; - } - - public NonSuccessfulResponseCodeException(int code, String s) { - super("[" + code + "] " + s); - this.code = code; - this.body = null; - } - - public NonSuccessfulResponseCodeException(int code, String s, String body) { - super("[" + code + "] " + s); - this.code = code; - this.body = body; - } - - public int getCode() { - return code; - } - - public boolean is4xx() { - return code >= 400 && code < 500; - } - - public boolean is5xx() { - return code >= 500 && code < 600; - } - - public String getBody() { - return body; - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.kt new file mode 100644 index 0000000000..8fcb608635 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.kt @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ +package org.whispersystems.signalservice.api.push.exceptions + +import java.io.IOException + +/** + * Indicates a server response that is not successful, typically something outside the 2xx range. + */ +open class NonSuccessfulResponseCodeException : IOException { + @JvmField + val code: Int + val stringBody: String? + val binaryBody: ByteArray? + + constructor(code: Int) : super("StatusCode: $code") { + this.code = code + this.stringBody = null + this.binaryBody = null + } + + constructor(code: Int, message: String) : super("[$code] $message") { + this.code = code + this.stringBody = null + this.binaryBody = null + } + + constructor(code: Int, message: String, body: String?) : super("[$code] $message") { + this.code = code + this.stringBody = body + this.binaryBody = null + } + + constructor(code: Int, message: String, body: ByteArray?) : super("[$code] $message") { + this.code = code + this.stringBody = null + this.binaryBody = body + } + + fun is4xx(): Boolean { + return code >= 400 && code < 500 + } + + fun is5xx(): Boolean { + return code >= 500 && code < 600 + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java index 3c8fac1ecf..ad1f32b891 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiV2Service.java @@ -89,7 +89,7 @@ public final class CdsiV2Service { .map(result -> ServiceResponse.forResult(result, 200, null)) .onErrorReturn(error -> { if (error instanceof NonSuccessfulResponseCodeException) { - int status = ((NonSuccessfulResponseCodeException) error).getCode(); + int status = ((NonSuccessfulResponseCodeException) error).code; return ServiceResponse.forApplicationError(error, status, null); } else { return ServiceResponse.forUnknownError(error); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java index 01181596c7..6871cf86f4 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java @@ -388,7 +388,7 @@ public class DonationsService { return ServiceResponse.forResult(responseAndCode.first(), responseAndCode.second(), null); } catch (NonSuccessfulResponseCodeException e) { Log.w(TAG, "Bad response code from server.", e); - return ServiceResponse.forApplicationError(e, e.getCode(), e.getMessage()); + return ServiceResponse.forApplicationError(e, e.code, e.getMessage()); } catch (IOException e) { Log.w(TAG, "An unknown error occurred.", e); return ServiceResponse.forUnknownError(e); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/RecordIkm.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/RecordIkm.kt new file mode 100644 index 0000000000..13f9d3d5ec --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/RecordIkm.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.storage + +import org.whispersystems.signalservice.api.crypto.Crypto +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord +import org.whispersystems.signalservice.internal.storage.protos.StorageItem +import org.whispersystems.signalservice.internal.util.Util +import org.whispersystems.util.StringUtil + +/** + * A wrapper around a [ByteArray], just so the recordIkm is strongly typed. + * The recordIkm comes from [ManifestRecord.recordIkm], and is used to encrypt [StorageItem.value_]. + */ +@JvmInline +value class RecordIkm(val value: ByteArray) { + + companion object { + fun generate(): RecordIkm { + return RecordIkm(Util.getSecretBytes(32)) + } + } + + fun deriveStorageItemKey(rawId: ByteArray): StorageItemKey { + val key = Crypto.hkdf( + inputKeyMaterial = this.value, + info = StringUtil.utf8("20240801_SIGNAL_STORAGE_SERVICE_ITEM_") + rawId, + outputLength = 32 + ) + + return StorageItemKey(key) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.kt index aea268928f..f5a8d777b5 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.kt @@ -1,18 +1,23 @@ package org.whispersystems.signalservice.api.storage +import okio.ByteString +import okio.ByteString.Companion.EMPTY +import okio.ByteString.Companion.toByteString +import org.signal.core.util.isNotEmpty import org.signal.core.util.toOptional import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord import org.whispersystems.signalservice.internal.storage.protos.StorageManifest import java.util.Optional -class SignalStorageManifest( +data class SignalStorageManifest( @JvmField val version: Long, - @JvmField val sourceDeviceId: Int, + val sourceDeviceId: Int, + val recordIkm: RecordIkm?, @JvmField val storageIds: List ) { companion object { - val EMPTY: SignalStorageManifest = SignalStorageManifest(0, 1, emptyList()) + val EMPTY: SignalStorageManifest = SignalStorageManifest(0, 1, null, emptyList()) fun deserialize(serialized: ByteArray): SignalStorageManifest { val manifest = StorageManifest.ADAPTER.decode(serialized) @@ -21,7 +26,12 @@ class SignalStorageManifest( StorageId.forType(id.raw.toByteArray(), id.typeValue) } - return SignalStorageManifest(manifest.version, manifestRecord.sourceDevice, ids) + return SignalStorageManifest( + version = manifest.version, + sourceDeviceId = manifestRecord.sourceDevice, + recordIkm = manifestRecord.recordIkm.takeIf { it.isNotEmpty() }?.toByteArray()?.let { RecordIkm(it) }, + storageIds = ids + ) } } @@ -40,7 +50,8 @@ class SignalStorageManifest( val manifestRecord = ManifestRecord( identifiers = ids, - sourceDevice = sourceDeviceId + sourceDevice = sourceDeviceId, + recordIkm = recordIkm?.value?.toByteString() ?: ByteString.EMPTY ) return StorageManifest( diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.kt deleted file mode 100644 index 1b9e37a8dd..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.whispersystems.signalservice.api.storage - -import okio.ByteString.Companion.toByteString -import org.signal.libsignal.protocol.InvalidKeyException -import org.signal.libsignal.protocol.logging.Log -import org.signal.libsignal.zkgroup.groups.GroupMasterKey -import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord -import org.whispersystems.signalservice.internal.storage.protos.StorageItem -import org.whispersystems.signalservice.internal.storage.protos.StorageManifest -import org.whispersystems.signalservice.internal.storage.protos.StorageRecord -import java.io.IOException - -object SignalStorageModels { - private val TAG: String = SignalStorageModels::class.java.simpleName - - @JvmStatic - @Throws(IOException::class, InvalidKeyException::class) - fun remoteToLocalStorageManifest(manifest: StorageManifest, storageKey: StorageKey): SignalStorageManifest { - val rawRecord = SignalStorageCipher.decrypt(storageKey.deriveManifestKey(manifest.version), manifest.value_.toByteArray()) - val manifestRecord = ManifestRecord.ADAPTER.decode(rawRecord) - val ids: List = manifestRecord.identifiers.map { id -> - StorageId.forType(id.raw.toByteArray(), id.typeValue) - } - - return SignalStorageManifest(manifestRecord.version, manifestRecord.sourceDevice, ids) - } - - @JvmStatic - @Throws(IOException::class, InvalidKeyException::class) - fun remoteToLocalStorageRecord(item: StorageItem, type: Int, storageKey: StorageKey): SignalStorageRecord { - val key = item.key.toByteArray() - val rawRecord = SignalStorageCipher.decrypt(storageKey.deriveItemKey(key), item.value_.toByteArray()) - val record = StorageRecord.ADAPTER.decode(rawRecord) - val id = StorageId.forType(key, type) - - if (record.contact != null && type == ManifestRecord.Identifier.Type.CONTACT.value) { - return SignalContactRecord(id, record.contact).toSignalStorageRecord() - } else if (record.groupV1 != null && type == ManifestRecord.Identifier.Type.GROUPV1.value) { - return SignalGroupV1Record(id, record.groupV1).toSignalStorageRecord() - } else if (record.groupV2 != null && type == ManifestRecord.Identifier.Type.GROUPV2.value && record.groupV2.masterKey.size == GroupMasterKey.SIZE) { - return SignalGroupV2Record(id, record.groupV2).toSignalStorageRecord() - } else if (record.account != null && type == ManifestRecord.Identifier.Type.ACCOUNT.value) { - return SignalAccountRecord(id, record.account).toSignalStorageRecord() - } else if (record.storyDistributionList != null && type == ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST.value) { - return SignalStoryDistributionListRecord(id, record.storyDistributionList).toSignalStorageRecord() - } else if (record.callLink != null && type == ManifestRecord.Identifier.Type.CALL_LINK.value) { - return SignalCallLinkRecord(id, record.callLink).toSignalStorageRecord() - } else { - if (StorageId.isKnownType(type)) { - Log.w(TAG, "StorageId is of known type ($type), but the data is bad! Falling back to unknown.") - } - return SignalStorageRecord.forUnknown(StorageId.forType(key, type)) - } - } - - @JvmStatic - fun localToRemoteStorageRecord(record: SignalStorageRecord, storageKey: StorageKey): StorageItem { - val itemKey = storageKey.deriveItemKey(record.id.raw) - val encryptedRecord = SignalStorageCipher.encrypt(itemKey, record.proto.encode()) - - return StorageItem.Builder() - .key(record.id.raw.toByteString()) - .value_(encryptedRecord.toByteString()) - .build() - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageKey.kt index 5a7ab67393..14e06286f5 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageKey.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageKey.kt @@ -20,8 +20,8 @@ class StorageKey(val key: ByteArray) { return StorageManifestKey(derive("Manifest_$version")) } - fun deriveItemKey(key: ByteArray): StorageItemKey { - return StorageItemKey(derive("Item_" + encodeWithPadding(key))) + fun deriveItemKey(rawId: ByteArray): StorageItemKey { + return StorageItemKey(derive("Item_" + encodeWithPadding(rawId))) } private fun derive(keyName: String): ByteArray { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceApi.kt new file mode 100644 index 0000000000..17f1551616 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceApi.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.storage + +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import org.whispersystems.signalservice.internal.storage.protos.ReadOperation +import org.whispersystems.signalservice.internal.storage.protos.StorageItems +import org.whispersystems.signalservice.internal.storage.protos.StorageManifest +import org.whispersystems.signalservice.internal.storage.protos.WriteOperation + +/** + * Class to interact with storage service endpoints. + */ +class StorageServiceApi(private val pushServiceSocket: PushServiceSocket) { + + /** + * Retrieves an auth string that's needed to make other storage requests. + * + * GET /v1/storage/auth + */ + fun getAuth(): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getStorageAuth() + } + } + + /** + * Gets the latest [StorageManifest]. + * + * GET /v1/storage/manifest + * + * - 200: Success + * - 404: No storage manifest was found + */ + fun getStorageManifest(authToken: String): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getStorageManifest(authToken) + } + } + + /** + * Gets the latest [StorageManifest], but only if the version supplied doesn't match the remote. + * + * GET /v1/storage/manifest/version/{version} + * + * - 200: Success + * - 204: The manifest matched the provided version, and therefore no manifest was returned + */ + fun getStorageManifestIfDifferentVersion(authToken: String, version: Long): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getStorageManifestIfDifferentVersion(authToken, version) + } + } + + /** + * PUT /v1/storage/read + */ + fun readStorageItems(authToken: String, operation: ReadOperation): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.readStorageItems(authToken, operation) + } + } + + /** + * Performs the provided [WriteOperation]. + * + * PUT /v1/storage + * + * - 200: Success + * - 409: Your [WriteOperation] version does not equal remoteVersion + 1. That means that there have been writes that you're not aware of. + * The body includes the current [StorageManifest] as binary data. + */ + fun writeStorageItems(authToken: String, writeOperation: WriteOperation): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.writeStorageItems(authToken, writeOperation) + } + } + + /** + * Lets you know if storage service is reachable. + * + * GET /ping + */ + fun pingStorageService(): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.pingStorageService() + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceRepository.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceRepository.kt new file mode 100644 index 0000000000..ae15b93d3e --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/StorageServiceRepository.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.storage + +import com.squareup.wire.FieldEncoding +import okio.ByteString +import okio.ByteString.Companion.toByteString +import okio.IOException +import org.signal.core.util.isNotEmpty +import org.signal.libsignal.protocol.InvalidKeyException +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord +import org.whispersystems.signalservice.internal.storage.protos.ReadOperation +import org.whispersystems.signalservice.internal.storage.protos.StorageItem +import org.whispersystems.signalservice.internal.storage.protos.StorageItems +import org.whispersystems.signalservice.internal.storage.protos.StorageManifest +import org.whispersystems.signalservice.internal.storage.protos.StorageRecord +import org.whispersystems.signalservice.internal.storage.protos.WriteOperation +import java.lang.Exception + +/** + * Collection of higher-level storage service operations. Each method tends to make multiple + * calls to [StorageServiceApi], wrapping the responses in easier-to-use result types. + */ +class StorageServiceRepository(private val storageServiceApi: StorageServiceApi) { + + companion object { + private const val STORAGE_READ_MAX_ITEMS: Int = 1000 + } + + /** + * Fetches the version of the remote manifest. + */ + fun getManifestVersion(): NetworkResult { + return storageServiceApi + .getAuth() + .then { storageServiceApi.getStorageManifest(it) } + .map { it.version } + } + + /** + * Fetches and returns the fully-decrypted [SignalStorageManifest], if possible. + * Note: You should prefer using [getStorageManifestIfDifferentVersion] when possible. + */ + fun getStorageManifest(storageKey: StorageKey): ManifestResult { + val manifest: StorageManifest = storageServiceApi + .getAuth() + .then { storageServiceApi.getStorageManifest(it) } + .let { result -> + when (result) { + is NetworkResult.Success -> result.result + is NetworkResult.ApplicationError -> throw result.throwable + is NetworkResult.NetworkError -> return ManifestResult.NetworkError(result.exception) + is NetworkResult.StatusCodeError -> { + return when (result.code) { + 404 -> ManifestResult.NotFoundError + else -> ManifestResult.StatusCodeError(result.code, result.exception) + } + } + } + } + + return try { + val decrypted = manifest.toLocal(storageKey) + ManifestResult.Success(decrypted) + } catch (e: InvalidKeyException) { + ManifestResult.DecryptionError(e) + } + } + + /** + * Fetches and returns the fully-decrypted [SignalStorageManifest] if the remote version is higher than the [manifestVersion] passed in. + * The intent is that you only need the manifest if it's newer than what you already have. + */ + fun getStorageManifestIfDifferentVersion(storageKey: StorageKey, manifestVersion: Long): ManifestIfDifferentVersionResult { + val manifest = storageServiceApi + .getAuth() + .then { storageServiceApi.getStorageManifestIfDifferentVersion(it, manifestVersion) } + .let { result -> + when (result) { + is NetworkResult.Success -> result.result + is NetworkResult.ApplicationError -> throw result.throwable + is NetworkResult.NetworkError -> return ManifestIfDifferentVersionResult.NetworkError(result.exception) + is NetworkResult.StatusCodeError -> { + return when (result.code) { + 204 -> ManifestIfDifferentVersionResult.SameVersion + else -> ManifestIfDifferentVersionResult.StatusCodeError(result.code, result.exception) + } + } + } + } + + return try { + val decrypted = manifest.toLocal(storageKey) + ManifestIfDifferentVersionResult.DifferentVersion(decrypted) + } catch (e: InvalidKeyException) { + ManifestIfDifferentVersionResult.DecryptionError(e) + } + } + + /** + * Fetches and returns the fully-decrypted [SignalStorageRecord]s matching the list of provided [storageIds] + */ + fun readStorageRecords(storageKey: StorageKey, recordIkm: RecordIkm?, storageIds: List): StorageRecordResult { + val auth: String = when (val result = storageServiceApi.getAuth()) { + is NetworkResult.Success -> result.result + is NetworkResult.ApplicationError -> throw result.throwable + is NetworkResult.NetworkError -> return StorageRecordResult.NetworkError(result.exception) + is NetworkResult.StatusCodeError -> return StorageRecordResult.StatusCodeError(result.code, result.exception) + } + + val knownIds = storageIds.filterNot { it.isUnknown } + val batches = knownIds.chunked(STORAGE_READ_MAX_ITEMS) + val results = batches.map { batch -> + readStorageRecordsBatch(auth, storageKey, recordIkm, batch) + } + + results + .firstOrNull { it !is StorageRecordResult.Success } + ?.let { firstFailedResult -> + return firstFailedResult + } + + val unknownRecordPlaceholders = storageIds + .filter { it.isUnknown } + .map { SignalStorageRecord.forUnknown(it) } + + val allResults = results + .map { (it as StorageRecordResult.Success).records } + .flatten() + unknownRecordPlaceholders + + return StorageRecordResult.Success(allResults) + } + + /** + * Writes the provided data to storage service. + */ + fun writeStorageRecords( + storageKey: StorageKey, + signalManifest: SignalStorageManifest, + insertItems: List, + deleteRawIds: List + ): WriteStorageRecordsResult { + return writeStorageRecords(storageKey, signalManifest, insertItems, deleteRawIds, clearAllExisting = false) + } + + /** + * Writes the provided data to storage service, overwriting _all other data_ in the process. + * Reserved for very specific circumstances! (Like fixing undecryptable data). + */ + fun resetAndWriteStorageRecords( + storageKey: StorageKey, + manifest: SignalStorageManifest, + insertItems: List + ): WriteStorageRecordsResult { + return writeStorageRecords(storageKey, manifest, insertItems, emptyList(), clearAllExisting = true) + } + + /** + * Writes the current manifest with no insertions or deletes. Intended to be done after rotating your AEP. + */ + fun writeUnchangedManifest(storageKey: StorageKey, manifest: SignalStorageManifest): WriteStorageRecordsResult { + return writeStorageRecords(storageKey, manifest, emptyList(), emptyList(), clearAllExisting = false) + } + + private fun writeStorageRecords( + storageKey: StorageKey, + signalManifest: SignalStorageManifest, + insertItems: List, + deleteRawIds: List, + clearAllExisting: Boolean + ): WriteStorageRecordsResult { + val manifestIds = signalManifest.storageIds.map { id -> + val builder = ManifestRecord.Identifier.Builder() + builder.raw = id.raw.toByteString() + if (id.isUnknown) { + builder.type = ManifestRecord.Identifier.Type.UNKNOWN + builder.addUnknownField(2, FieldEncoding.VARINT, id.type) + } else { + builder.type(ManifestRecord.Identifier.Type.fromValue(id.type)!!) + } + builder.build() + } + + val manifestRecord = ManifestRecord( + sourceDevice = signalManifest.sourceDeviceId, + version = signalManifest.version, + identifiers = manifestIds + ) + + val manifestKey = storageKey.deriveManifestKey(signalManifest.version) + + val encryptedManifest = StorageManifest( + version = manifestRecord.version, + value_ = SignalStorageCipher.encrypt(manifestKey, manifestRecord.encode()).toByteString() + ) + + val writeOperation = WriteOperation.Builder().apply { + manifest = encryptedManifest + insertItem = insertItems.map { it.toRemote(storageKey, signalManifest.recordIkm) } + + if (clearAllExisting) { + clearAll = true + } else { + deleteKey = deleteRawIds.map { it.toByteString() } + } + }.build() + + val result = storageServiceApi + .getAuth() + .then { auth -> storageServiceApi.writeStorageItems(auth, writeOperation) } + + return when (result) { + is NetworkResult.Success -> WriteStorageRecordsResult.Success + is NetworkResult.ApplicationError -> throw result.throwable + is NetworkResult.NetworkError -> WriteStorageRecordsResult.NetworkError(result.exception) + is NetworkResult.StatusCodeError -> { + when (result.code) { + 409 -> WriteStorageRecordsResult.ConflictError + else -> WriteStorageRecordsResult.StatusCodeError(result.code, result.exception) + } + } + } + } + + private fun readStorageRecordsBatch(auth: String, storageKey: StorageKey, recordIkm: RecordIkm?, storageIds: List): StorageRecordResult { + check(storageIds.size <= STORAGE_READ_MAX_ITEMS) + check(storageIds.none { it.isUnknown }) + + val typesByKey: Map = storageIds.associate { it.raw.toByteString() to it.type } + + val readOperation = ReadOperation( + readKey = storageIds.map { it.raw.toByteString() } + ) + + val storageItems: StorageItems = storageServiceApi + .readStorageItems(auth, readOperation) + .let { itemResult -> + when (itemResult) { + is NetworkResult.Success -> itemResult.result + is NetworkResult.ApplicationError -> throw itemResult.throwable + is NetworkResult.NetworkError -> return StorageRecordResult.NetworkError(itemResult.exception) + is NetworkResult.StatusCodeError -> return StorageRecordResult.StatusCodeError(itemResult.code, itemResult.exception) + } + } + + return try { + val items = storageItems.items.map { item -> + val type = typesByKey[item.key]!! + item.toLocal(type, storageKey, recordIkm) + } + StorageRecordResult.Success(items) + } catch (e: InvalidKeyException) { + StorageRecordResult.DecryptionError(e) + } + } + + @Throws(IOException::class, InvalidKeyException::class) + private fun StorageManifest.toLocal(storageKey: StorageKey): SignalStorageManifest { + val rawRecord = SignalStorageCipher.decrypt(storageKey.deriveManifestKey(this.version), this.value_.toByteArray()) + val manifestRecord = ManifestRecord.ADAPTER.decode(rawRecord) + val ids: List = manifestRecord.identifiers.map { id -> + StorageId.forType(id.raw.toByteArray(), id.typeValue) + } + + return SignalStorageManifest( + version = manifestRecord.version, + sourceDeviceId = manifestRecord.sourceDevice, + recordIkm = manifestRecord.recordIkm.takeIf { it.isNotEmpty() }?.toByteArray()?.let { RecordIkm(it) }, + storageIds = ids + ) + } + + @Throws(IOException::class, InvalidKeyException::class) + private fun StorageItem.toLocal(type: Int, storageKey: StorageKey, recordIkm: RecordIkm?): SignalStorageRecord { + val rawId = this.key.toByteArray() + val key = recordIkm?.deriveStorageItemKey(rawId) ?: storageKey.deriveItemKey(rawId) + val rawRecord = SignalStorageCipher.decrypt(key, this.value_.toByteArray()) + val record = StorageRecord.ADAPTER.decode(rawRecord) + val id = StorageId.forType(rawId, type) + + return SignalStorageRecord(id, record) + } + + private fun SignalStorageRecord.toRemote(storageKey: StorageKey, recordIkm: RecordIkm?): StorageItem { + val key = recordIkm?.deriveStorageItemKey(this.id.raw) ?: storageKey.deriveItemKey(this.id.raw) + val encryptedRecord = SignalStorageCipher.encrypt(key, this.proto.encode()) + + return StorageItem.Builder() + .key(this.id.raw.toByteString()) + .value_(encryptedRecord.toByteString()) + .build() + } + + sealed interface WriteStorageRecordsResult { + data object Success : WriteStorageRecordsResult + data class NetworkError(val exception: IOException) : WriteStorageRecordsResult + data object ConflictError : WriteStorageRecordsResult + data class StatusCodeError(val code: Int, val exception: NonSuccessfulResponseCodeException) : WriteStorageRecordsResult + } + + sealed interface ManifestResult { + data class Success(val manifest: SignalStorageManifest) : ManifestResult + data class NetworkError(val exception: IOException) : ManifestResult + data class DecryptionError(val exception: Exception) : ManifestResult + data object NotFoundError : ManifestResult + data class StatusCodeError(val code: Int, val exception: NonSuccessfulResponseCodeException) : ManifestResult + } + + sealed interface ManifestIfDifferentVersionResult { + data class DifferentVersion(val manifest: SignalStorageManifest) : ManifestIfDifferentVersionResult + data object SameVersion : ManifestIfDifferentVersionResult + data class NetworkError(val exception: IOException) : ManifestIfDifferentVersionResult + data class DecryptionError(val exception: Exception) : ManifestIfDifferentVersionResult + data class StatusCodeError(val code: Int, val exception: NonSuccessfulResponseCodeException) : ManifestIfDifferentVersionResult + } + + sealed interface StorageRecordResult { + data class Success(val records: List) : StorageRecordResult + data class NetworkError(val exception: IOException) : StorageRecordResult + data class DecryptionError(val exception: Exception) : StorageRecordResult + data class StatusCodeError(val code: Int, val exception: NonSuccessfulResponseCodeException) : StorageRecordResult + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java index 11ab1ee49f..026e2bb7ac 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java @@ -120,7 +120,7 @@ public final class ServiceResponse { if (throwable instanceof ExecutionException) { return forUnknownError(throwable.getCause()); } else if (throwable instanceof NonSuccessfulResponseCodeException) { - return forApplicationError(throwable, ((NonSuccessfulResponseCodeException) throwable).getCode(), null); + return forApplicationError(throwable, ((NonSuccessfulResponseCodeException) throwable).code, null); } else if (throwable instanceof PushNetworkException && throwable.getCause() != null) { return forUnknownError(throwable.getCause()); } else { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 8658ed956d..330deffa96 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -347,10 +347,11 @@ public class PushServiceSocket { private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth"; private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; - private static final Map NO_HEADERS = Collections.emptyMap(); - private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler(); - private static final ResponseCodeHandler UNOPINIONATED_HANDLER = new UnopinionatedResponseCodeHandler(); - private static final ResponseCodeHandler LONG_POLL_HANDLER = new LongPollingResponseCodeHandler(); + private static final Map NO_HEADERS = Collections.emptyMap(); + private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler(); + private static final ResponseCodeHandler LONG_POLL_HANDLER = new LongPollingResponseCodeHandler(); + private static final ResponseCodeHandler UNOPINIONATED_HANDLER = new UnopinionatedResponseCodeHandler(); + private static final ResponseCodeHandler UNOPINIONATED_BINARY_ERROR_HANDLER = new UnopinionatedBinaryErrorResponseCodeHandler(); public static final long CDN2_RESUMABLE_LINK_LIFETIME_MILLIS = TimeUnit.DAYS.toMillis(7); @@ -1608,6 +1609,10 @@ public class PushServiceSocket { } } + public void writeStorageItems(String authToken, WriteOperation writeOperation) throws IOException { + makeStorageRequest(authToken, "/v1/storage", "PUT", protobufRequestBody(writeOperation), UNOPINIONATED_BINARY_ERROR_HANDLER); + } + public void pingStorageService() throws IOException { try (Response response = makeStorageRequest(null, "/ping", "GET", null, NO_HANDLER)) { return; @@ -2863,6 +2868,24 @@ public class PushServiceSocket { } } + /** + * A {@link ResponseCodeHandler} that only throws {@link NonSuccessfulResponseCodeException} with the response body. + * Any further processing is left to the caller. + */ + private static class UnopinionatedBinaryErrorResponseCodeHandler implements ResponseCodeHandler { + @Override + public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + if (responseCode < 200 || responseCode > 299) { + byte[] bodyBytes = null; + if (body != null) { + bodyBytes = readBodyBytes(body); + } + + throw new NonSuccessfulResponseCodeException(responseCode, "Response: " + responseCode, bodyBytes); + } + } + } + public enum ClientSet { KeyBackup } public CredentialResponse retrieveGroupsV2Credentials(long todaySeconds) @@ -2881,16 +2904,19 @@ public class PushServiceSocket { private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = (responseCode, body) -> { if (responseCode == 409) throw new GroupExistsException(); - };; + }; + private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = (responseCode, body) -> { switch (responseCode) { case 403: throw new NotInGroupException(); case 404: throw new GroupNotFoundException(); } }; + private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = (responseCode, body) -> { if (responseCode == 400) throw new GroupPatchNotAcceptedException(); }; + private static final ResponseCodeHandler GROUPS_V2_GET_JOIN_INFO_HANDLER = new ResponseCodeHandler() { @Override public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException { diff --git a/libsignal-service/src/main/protowire/SignalService.proto b/libsignal-service/src/main/protowire/SignalService.proto index 343092d1d9..fcdfa96d88 100644 --- a/libsignal-service/src/main/protowire/SignalService.proto +++ b/libsignal-service/src/main/protowire/SignalService.proto @@ -546,7 +546,10 @@ message SyncMessage { message Keys { // @deprecated optional bytes storageService = 1; - optional bytes master = 2; + // @deprecated + optional bytes master = 2; + optional string accountEntropyPool = 3; + optional bytes mediaRootBackupKey = 4; } message MessageRequestResponse { diff --git a/libsignal-service/src/main/protowire/StorageService.proto b/libsignal-service/src/main/protowire/StorageService.proto index 7d01e0e40b..59d87e8d7d 100644 --- a/libsignal-service/src/main/protowire/StorageService.proto +++ b/libsignal-service/src/main/protowire/StorageService.proto @@ -60,7 +60,8 @@ message ManifestRecord { uint64 version = 1; uint32 sourceDevice = 3; repeated Identifier identifiers = 2; - // Next ID: 4 + bytes recordIkm = 4; + // Next ID: 5 } message StorageRecord { diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/NetworkResultTest.kt b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/NetworkResultTest.kt index 591ee17d11..da6f44f613 100644 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/NetworkResultTest.kt +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/NetworkResultTest.kt @@ -31,7 +31,7 @@ class NetworkResultTest { check(result is NetworkResult.StatusCodeError) assertEquals(exception, result.exception) assertEquals(404, result.code) - assertEquals("body", result.body) + assertEquals("body", result.stringBody) } @Test diff --git a/spinner/lib/src/main/assets/plugin.hbs b/spinner/lib/src/main/assets/plugin.hbs index 91169668e8..bb7b37543c 100644 --- a/spinner/lib/src/main/assets/plugin.hbs +++ b/spinner/lib/src/main/assets/plugin.hbs @@ -23,6 +23,9 @@ {{/each}} {{/if}} + {{#if (eq "string" pluginResult.type)}} +

{{pluginResult.text}}

+ {{/if}} {{> partials/suffix }} diff --git a/spinner/lib/src/main/java/org/signal/spinner/PluginResult.kt b/spinner/lib/src/main/java/org/signal/spinner/PluginResult.kt index a60a4d98db..26b845ada7 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/PluginResult.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/PluginResult.kt @@ -6,4 +6,8 @@ sealed class PluginResult(val type: String) { val rows: List>, val rowCount: Int = rows.size ) : PluginResult("table") + + data class StringResult( + val text: String + ) : PluginResult("string") }