Introduce AEP and SSRE2.

This commit is contained in:
Greyson Parrelli
2024-11-18 13:12:58 -05:00
parent 1401256ffd
commit 1b2c0db693
60 changed files with 1162 additions and 511 deletions

View File

@@ -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
)
}
}

View File

@@ -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.
}

View File

@@ -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())
)
}

View File

@@ -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
)
}
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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());

View File

@@ -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
)
)

View File

@@ -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 +

View File

@@ -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) {

View File

@@ -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<SignalStorageRecord> = 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.")

View File

@@ -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<RecipientId, StorageId> = 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()

View File

@@ -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<StorageRotateManifestJob?> {
override fun create(parameters: Parameters, serializedData: ByteArray?): StorageRotateManifestJob {
return StorageRotateManifestJob(parameters)
}
}
}

View File

@@ -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.")

View File

@@ -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))

View File

@@ -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.

View File

@@ -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<String> = 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()
}
}
}
}

View File

@@ -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)
}

View File

@@ -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!");

View File

@@ -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<AepMigrationJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): AepMigrationJob {
return AepMigrationJob(parameters)
}
}
}

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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<BackupResponse.Success>()
.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()

View File

@@ -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

View File

@@ -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,

View File

@@ -81,13 +81,13 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
}
private fun createChallengeRequiredProcessor(errorResult: NetworkResult.StatusCodeError<RegistrationSessionMetadataResponse>): 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)

View File

@@ -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,

View File

@@ -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
}