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

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

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
}

View file

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

View file

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

View file

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

View file

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

View file

@ -105,8 +105,8 @@ sealed class NetworkResult<T>(
data class NetworkError<T>(val exception: IOException) : NetworkResult<T>()
/** Indicates we got a response, but it was a non-2xx response. */
data class StatusCodeError<T>(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult<T>() {
constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.body, e)
data class StatusCodeError<T>(val code: Int, val stringBody: String?, val binaryBody: ByteArray?, val exception: NonSuccessfulResponseCodeException) : NetworkResult<T>() {
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<T>(
* 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<LocalUserModel> = NetworkResult
* .fromFetch { fetchRemoteUserModel() }
@ -151,10 +153,16 @@ sealed class NetworkResult<T>(
*/
fun <R> map(transform: (T) -> R): NetworkResult<R> {
return when (this) {
is Success -> Success(transform(this.result)).runOnStatusCodeError(statusCodeErrorActions)
is Success -> {
try {
Success(transform(this.result)).runOnStatusCodeError(statusCodeErrorActions)
} catch (e: Throwable) {
ApplicationError<R>(e).runOnStatusCodeError(statusCodeErrorActions)
}
}
is NetworkError -> NetworkError<R>(exception).runOnStatusCodeError(statusCodeErrorActions)
is ApplicationError -> ApplicationError<R>(throwable).runOnStatusCodeError(statusCodeErrorActions)
is StatusCodeError -> StatusCodeError<R>(code, body, exception).runOnStatusCodeError(statusCodeErrorActions)
is StatusCodeError -> StatusCodeError<R>(code, stringBody, binaryBody, exception).runOnStatusCodeError(statusCodeErrorActions)
}
}
@ -204,7 +212,7 @@ sealed class NetworkResult<T>(
is Success -> result(this.result).runOnStatusCodeError(statusCodeErrorActions)
is NetworkError -> NetworkError<R>(exception).runOnStatusCodeError(statusCodeErrorActions)
is ApplicationError -> ApplicationError<R>(throwable).runOnStatusCodeError(statusCodeErrorActions)
is StatusCodeError -> StatusCodeError<R>(code, body, exception).runOnStatusCodeError(statusCodeErrorActions)
is StatusCodeError -> StatusCodeError<R>(code, stringBody, binaryBody, exception).runOnStatusCodeError(statusCodeErrorActions)
}
}

View file

@ -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<SignalStorageManifest> 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<SignalStorageManifest> 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<SignalStorageRecord> readStorageRecords(StorageKey storageKey, List<StorageId> storageKeys) throws IOException, InvalidKeyException {
if (storageKeys.isEmpty()) {
return Collections.emptyList();
}
List<SignalStorageRecord> result = new ArrayList<>();
Map<ByteString, Integer> typeMap = new HashMap<>();
List<ReadOperation> readOperations = new LinkedList<>();
List<ByteString> 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<SignalStorageManifest> resetStorageRecords(StorageKey storageKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> allRecords)
throws IOException, InvalidKeyException
{
return writeStorageRecords(storageKey, manifest, allRecords, Collections.<byte[]>emptyList(), true);
}
/**
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
*/
public Optional<SignalStorageManifest> writeStorageRecords(StorageKey storageKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> inserts,
List<byte[]> 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<SignalStorageManifest> writeStorageRecords(StorageKey storageKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> inserts,
List<byte[]> 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<StorageManifest> 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<StorageId> 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<String, Object> out = new HashMap<>();

View file

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

View file

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

View file

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

View file

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

View file

@ -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<StorageKey> storageService;
private final Optional<MasterKey> master;
public KeysMessage(Optional<StorageKey> storageService, Optional<MasterKey> master) {
this.storageService = storageService;
this.master = master;
}
public Optional<StorageKey> getStorageService() {
return storageService;
}
public Optional<MasterKey> getMaster() {
return master;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String> {
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<StorageManifest> {
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<StorageManifest> {
return NetworkResult.fromFetch {
pushServiceSocket.getStorageManifestIfDifferentVersion(authToken, version)
}
}
/**
* PUT /v1/storage/read
*/
fun readStorageItems(authToken: String, operation: ReadOperation): NetworkResult<StorageItems> {
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<Unit> {
return NetworkResult.fromFetch {
pushServiceSocket.writeStorageItems(authToken, writeOperation)
}
}
/**
* Lets you know if storage service is reachable.
*
* GET /ping
*/
fun pingStorageService(): NetworkResult<Unit> {
return NetworkResult.fromFetch {
pushServiceSocket.pingStorageService()
}
}
}

View file

@ -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<Long> {
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<StorageId>): 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<SignalStorageRecord>,
deleteRawIds: List<ByteArray>
): 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<SignalStorageRecord>
): 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<SignalStorageRecord>,
deleteRawIds: List<ByteArray>,
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<StorageId>): StorageRecordResult {
check(storageIds.size <= STORAGE_READ_MAX_ITEMS)
check(storageIds.none { it.isUnknown })
val typesByKey: Map<ByteString, Int> = 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<StorageId> = 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<SignalStorageRecord>) : 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
}
}

View file

@ -120,7 +120,7 @@ public final class ServiceResponse<Result> {
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 {

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,9 @@
{{/each}}
</table>
{{/if}}
{{#if (eq "string" pluginResult.type)}}
<p>{{pluginResult.text}}</p>
{{/if}}
{{> partials/suffix }}
</body>
</html>

View file

@ -6,4 +6,8 @@ sealed class PluginResult(val type: String) {
val rows: List<List<String>>,
val rowCount: Int = rows.size
) : PluginResult("table")
data class StringResult(
val text: String
) : PluginResult("string")
}