diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index 56aca551de..3b65b0e7a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.SignalServiceMessageSender import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest +import org.whispersystems.signalservice.api.account.PreKeyUpload import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceIdType @@ -244,10 +245,19 @@ class ChangeNumberRepository( SignalStore.account().setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair) val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId) - val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(pniProtocolStore, pniMetadataStore) + val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(pniProtocolStore, pniMetadataStore) pniMetadataStore.activeSignedPreKeyId = signedPreKey.id - accountManager.setPreKeys(ServiceIdType.PNI, pniProtocolStore.identityKeyPair.publicKey, signedPreKey, oneTimePreKeys) + accountManager.setPreKeys( + PreKeyUpload( + serviceIdType = ServiceIdType.PNI, + identityKey = pniProtocolStore.identityKeyPair.publicKey, + signedPreKey = signedPreKey, + oneTimeEcPreKeys = oneTimePreKeys, + lastResortKyberPreKey = null, + oneTimeKyberPreKeys = null + ) + ) pniMetadataStore.isSignedPreKeyRegistered = true pniProtocolStore.identities().saveIdentityWithoutSideEffects( diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java index 78fd404aec..9d1830c9c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -25,11 +25,15 @@ import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.signal.libsignal.protocol.kem.KEMKeyPair; +import org.signal.libsignal.protocol.kem.KEMKeyType; +import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignalProtocolStore; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.Medium; import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; +import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import java.util.Comparator; import java.util.LinkedList; @@ -44,11 +48,11 @@ public class PreKeyUtil { private static final int BATCH_SIZE = 100; private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30); - public synchronized static @NonNull List generateAndStoreOneTimePreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { - Log.i(TAG, "Generating one-time prekeys..."); + public synchronized static @NonNull List generateAndStoreOneTimeEcPreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { + Log.i(TAG, "Generating one-time EC prekeys..."); List records = new LinkedList<>(); - int preKeyIdOffset = metadataStore.getNextOneTimePreKeyId(); + int preKeyIdOffset = metadataStore.getNextEcOneTimePreKeyId(); for (int i = 0; i < BATCH_SIZE; i++) { int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; @@ -59,7 +63,27 @@ public class PreKeyUtil { records.add(record); } - metadataStore.setNextOneTimePreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE); + metadataStore.setNextEcOneTimePreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE); + + return records; + } + + public synchronized static @NonNull List generateAndStoreOneTimeKyberPreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { + Log.i(TAG, "Generating one-time kyber prekeys..."); + + List records = new LinkedList<>(); + int preKeyIdOffset = metadataStore.getNextKyberPreKeyId(); + + + for (int i = 0; i < BATCH_SIZE; i++) { + int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; + KyberPreKeyRecord record = generateKyberPreKey(preKeyId, protocolStore.getIdentityKeyPair().getPrivateKey()); + + protocolStore.storeKyberPreKey(preKeyId, record); + records.add(record); + } + + metadataStore.setNextKyberPreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE); return records; } @@ -94,6 +118,31 @@ public class PreKeyUtil { } } + public synchronized static @NonNull KyberPreKeyRecord generateAndStoreLastResortKyberPreKey(@NonNull SignalServiceAccountDataStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { + return generateAndStoreLastResortKyberPreKey(protocolStore, metadataStore, protocolStore.getIdentityKeyPair().getPrivateKey()); + } + + public synchronized static @NonNull KyberPreKeyRecord generateAndStoreLastResortKyberPreKey(@NonNull SignalServiceAccountDataStore protocolStore, + @NonNull PreKeyMetadataStore metadataStore, + @NonNull ECPrivateKey privateKey) + { + int id = metadataStore.getNextKyberPreKeyId(); + KyberPreKeyRecord record = generateKyberPreKey(id, privateKey); + + protocolStore.storeKyberPreKey(id, record); + metadataStore.setNextKyberPreKeyId((id + 1) % Medium.MAX_VALUE); + + return record; + } + + public synchronized static @NonNull KyberPreKeyRecord generateKyberPreKey(int id, @NonNull ECPrivateKey privateKey) { + KEMKeyPair keyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024); + byte[] signature = privateKey.calculateSignature(keyPair.getPublicKey().serialize()); + + return new KyberPreKeyRecord(id, System.currentTimeMillis(), keyPair, signature); + } + + /** * Finds all of the signed prekeys that are older than the archive age, and archive all but the youngest of those. */ @@ -123,4 +172,34 @@ public class PreKeyUtil { Log.w(TAG, e); } } + + /** + * Finds all of the signed prekeys that are older than the archive age, and archive all but the youngest of those. + */ + public synchronized static void cleanLastResortKyberPreKeys(@NonNull SignalServiceAccountDataStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) { + Log.i(TAG, "Cleaning kyber prekeys..."); + + int activeLastResortKeyId = metadataStore.getLastResortKyberPreKeyId(); + if (activeLastResortKeyId < 0) { + return; + } + + try { + long now = System.currentTimeMillis(); + KyberPreKeyRecord currentRecord = protocolStore.loadKyberPreKey(activeLastResortKeyId); + List allRecords = protocolStore.loadLastResortKyberPreKeys(); + + allRecords.stream() + .filter(r -> r.getId() != currentRecord.getId()) + .filter(r -> (now - r.getTimestamp()) > ARCHIVE_AGE) + .sorted(Comparator.comparingLong(KyberPreKeyRecord::getTimestamp).reversed()) + .skip(1) + .forEach(record -> { + Log.i(TAG, "Removing kyber prekey record: " + record.getId() + " with timestamp: " + record.getTimestamp()); + protocolStore.removeKyberPreKey(record.getId()); + }); + } catch (InvalidKeyIdException e) { + Log.w(TAG, e); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/PreKeyMetadataStore.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/PreKeyMetadataStore.kt index efba5b20a2..36ae32ac4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/PreKeyMetadataStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/PreKeyMetadataStore.kt @@ -8,5 +8,8 @@ interface PreKeyMetadataStore { var activeSignedPreKeyId: Int var isSignedPreKeyRegistered: Boolean var lastSignedPreKeyRotationTime: Long - var nextOneTimePreKeyId: Int + var nextEcOneTimePreKeyId: Int + var nextKyberPreKeyId: Int + var lastResortKyberPreKeyId: Int + var lastResortKyberPreKeyRotationTime: Long } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalKyberPreKeyStore.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalKyberPreKeyStore.kt index f3ca9045a4..ba1de8e17b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalKyberPreKeyStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalKyberPreKeyStore.kt @@ -10,13 +10,14 @@ import org.signal.libsignal.protocol.state.KyberPreKeyRecord import org.signal.libsignal.protocol.state.KyberPreKeyStore import org.thoughtcrime.securesms.crypto.ReentrantSessionLock import org.thoughtcrime.securesms.database.SignalDatabase +import org.whispersystems.signalservice.api.SignalServiceKyberPreKeyStore import org.whispersystems.signalservice.api.push.ServiceId import kotlin.jvm.Throws /** * An implementation of the [KyberPreKeyStore] that stores entries in [org.thoughtcrime.securesms.database.KyberPreKeyTable]. */ -class SignalKyberPreKeyStore(private val selfServiceId: ServiceId) : KyberPreKeyStore { +class SignalKyberPreKeyStore(private val selfServiceId: ServiceId) : SignalServiceKyberPreKeyStore { @Throws(InvalidKeyIdException::class) override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord { @@ -31,8 +32,22 @@ class SignalKyberPreKeyStore(private val selfServiceId: ServiceId) : KyberPreKey } } + override fun loadLastResortKyberPreKeys(): List { + ReentrantSessionLock.INSTANCE.acquire().use { + return SignalDatabase.kyberPreKeys.getAllLastResort(selfServiceId).map { it.record } + } + } + override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord) { - error("This method is only used in tests") + ReentrantSessionLock.INSTANCE.acquire().use { + return SignalDatabase.kyberPreKeys.insert(selfServiceId, kyberPreKeyId, record, false) + } + } + + override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) { + ReentrantSessionLock.INSTANCE.acquire().use { + return SignalDatabase.kyberPreKeys.insert(selfServiceId, kyberPreKeyId, kyberPreKeyRecord, true) + } } override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean { @@ -46,4 +61,10 @@ class SignalKyberPreKeyStore(private val selfServiceId: ServiceId) : KyberPreKey SignalDatabase.kyberPreKeys.deleteIfNotLastResort(selfServiceId, kyberPreKeyId) } } + + override fun removeKyberPreKey(kyberPreKeyId: Int) { + ReentrantSessionLock.INSTANCE.acquire().use { + SignalDatabase.kyberPreKeys.delete(selfServiceId, kyberPreKeyId) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java index f7a246e9bd..97ba3202f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalServiceAccountDataStoreImpl.java @@ -181,11 +181,21 @@ public class SignalServiceAccountDataStoreImpl implements SignalServiceAccountDa return kyberPreKeyStore.loadKyberPreKeys(); } + @Override + public @NonNull List loadLastResortKyberPreKeys() { + return kyberPreKeyStore.loadLastResortKyberPreKeys(); + } + @Override public void storeKyberPreKey(int kyberPreKeyId, KyberPreKeyRecord record) { kyberPreKeyStore.storeKyberPreKey(kyberPreKeyId, record); } + @Override + public void storeLastResortKyberPreKey(int kyberPreKeyId, @NonNull KyberPreKeyRecord kyberPreKeyRecord) { + kyberPreKeyStore.storeKyberPreKey(kyberPreKeyId, kyberPreKeyRecord); + } + @Override public boolean containsKyberPreKey(int kyberPreKeyId) { return kyberPreKeyStore.containsKyberPreKey(kyberPreKeyId); @@ -196,6 +206,11 @@ public class SignalServiceAccountDataStoreImpl implements SignalServiceAccountDa kyberPreKeyStore.markKyberPreKeyUsed(kyberPreKeyId); } + @Override + public void removeKyberPreKey(int kyberPreKeyId) { + kyberPreKeyStore.removeKyberPreKey(kyberPreKeyId); + } + @Override public void storeSenderKey(SignalProtocolAddress sender, UUID distributionId, SenderKeyRecord record) { senderKeyStore.storeSenderKey(sender, distributionId, record); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/KyberPreKeyTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/KyberPreKeyTable.kt index a96ffd1aed..dad16ae710 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/KyberPreKeyTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/KyberPreKeyTable.kt @@ -9,6 +9,7 @@ import org.signal.core.util.readToSingleObject import org.signal.core.util.requireBoolean import org.signal.core.util.requireNonNullBlob import org.signal.core.util.select +import org.signal.core.util.toInt import org.signal.libsignal.protocol.state.KyberPreKeyRecord import org.whispersystems.signalservice.api.push.ServiceId @@ -71,6 +72,20 @@ class KyberPreKeyTable(context: Context, databaseHelper: SignalDatabase) : Datab } } + fun getAllLastResort(serviceId: ServiceId): List { + return readableDatabase + .select(LAST_RESORT, SERIALIZED) + .from("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY") + .where("$ACCOUNT_ID = ? AND $LAST_RESORT = ?", serviceId, 1) + .run() + .readToList { cursor -> + KyberPreKey( + record = KyberPreKeyRecord(cursor.requireNonNullBlob(SERIALIZED)), + lastResort = cursor.requireBoolean(LAST_RESORT) + ) + } + } + fun contains(serviceId: ServiceId, keyId: Int): Boolean { return readableDatabase .exists("$TABLE_NAME INDEXED BY $INDEX_ACCOUNT_KEY") @@ -78,14 +93,15 @@ class KyberPreKeyTable(context: Context, databaseHelper: SignalDatabase) : Datab .run() } - fun insert(serviceId: ServiceId, keyId: Int, record: KyberPreKeyRecord) { + fun insert(serviceId: ServiceId, keyId: Int, record: KyberPreKeyRecord, lastResort: Boolean) { writableDatabase .insertInto(TABLE_NAME) .values( ACCOUNT_ID to serviceId.toString(), KEY_ID to keyId, TIMESTAMP to record.timestamp, - SERIALIZED to record.serialize() + SERIALIZED to record.serialize(), + LAST_RESORT to lastResort.toInt() ) .run(SQLiteDatabase.CONFLICT_REPLACE) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java index b59c06bab5..ad97b7088b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java @@ -95,7 +95,7 @@ public final class PreKeyMigrationHelper { reader.close(); Log.i(TAG, "Setting next prekey id: " + index.nextPreKeyId); - SignalStore.account().aciPreKeys().setNextOneTimePreKeyId(index.nextPreKeyId); + SignalStore.account().aciPreKeys().setNextEcOneTimePreKeyId(index.nextPreKeyId); } catch (IOException e) { Log.w(TAG, e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt index 138149ec03..da32c202da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.jobs import androidx.annotation.VisibleForTesting import org.signal.core.util.logging.Log +import org.signal.libsignal.protocol.state.KyberPreKeyRecord +import org.signal.libsignal.protocol.state.PreKeyRecord import org.signal.libsignal.protocol.state.SignalProtocolStore import org.signal.libsignal.protocol.state.SignedPreKeyRecord import org.thoughtcrime.securesms.crypto.PreKeyUtil @@ -10,10 +12,13 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.whispersystems.signalservice.api.SignalServiceAccountDataStore +import org.whispersystems.signalservice.api.account.PreKeyUpload import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceIdType import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException +import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds @@ -24,7 +29,10 @@ import kotlin.time.DurationUnit * attempt to make the state valid. * * It will rotate/create signed prekeys for both ACI and PNI identities, as well as ensure that the user - * has a sufficient number of one-time prekeys available on the service. + * has a sufficient number of one-time EC prekeys available on the service. + * + * It will also rotate/create last-resort kyber prekeys for both ACI and PNI identities, as well as ensure + * that the user has a sufficient number of one-time kyber prekeys available on the service. */ class PreKeysSyncJob private constructor(parameters: Parameters) : BaseJob(parameters) { @@ -33,14 +41,14 @@ class PreKeysSyncJob private constructor(parameters: Parameters) : BaseJob(param private val TAG = Log.tag(PreKeysSyncJob::class.java) - /** The minimum number of one-time prekeys we want to the service to have. If we have less than this, refill. */ + /** The minimum number of one-time prekeys we want to the service to have. If we have less than this, refill. Applies to both EC and kyber prekeys. */ private const val ONE_TIME_PREKEY_MINIMUM = 10 - /** How often we want to rotate signed prekeys. */ + /** How often we want to rotate signed prekeys and last-resort kyber prekeys. */ @JvmField val REFRESH_INTERVAL = 2.days.inWholeMilliseconds - /** If signed prekeys are older than this, we will require rotation before sending messages. */ + /** If signed prekeys or last-resort kyber keys are older than this, we will require rotation before sending messages. */ @JvmField val MAXIMUM_ALLOWED_SIGNED_PREKEY_AGE = 14.days.inWholeMilliseconds @@ -57,11 +65,14 @@ class PreKeysSyncJob private constructor(parameters: Parameters) : BaseJob(param @JvmStatic fun enqueueIfNeeded() { if (!SignalStore.account().aciPreKeys.isSignedPreKeyRegistered || !SignalStore.account().pniPreKeys.isSignedPreKeyRegistered) { - Log.i(TAG, "Some signed prekeys aren't registered yet. Enqueuing a job. ACI: ${SignalStore.account().aciPreKeys.isSignedPreKeyRegistered} PNI: ${SignalStore.account().pniPreKeys.isSignedPreKeyRegistered}") + Log.i(TAG, "Some signed/last-resort prekeys aren't registered yet. Enqueuing a job. ACI: ${SignalStore.account().aciPreKeys.isSignedPreKeyRegistered} PNI: ${SignalStore.account().pniPreKeys.isSignedPreKeyRegistered}") ApplicationDependencies.getJobManager().add(PreKeysSyncJob()) } else if (SignalStore.account().aciPreKeys.activeSignedPreKeyId < 0 || SignalStore.account().pniPreKeys.activeSignedPreKeyId < 0) { Log.i(TAG, "Some signed prekeys aren't active yet. Enqueuing a job. ACI: ${SignalStore.account().aciPreKeys.activeSignedPreKeyId >= 0} PNI: ${SignalStore.account().pniPreKeys.activeSignedPreKeyId >= 0}") ApplicationDependencies.getJobManager().add(PreKeysSyncJob()) + } else if (SignalStore.account().aciPreKeys.lastResortKyberPreKeyId < 0 || SignalStore.account().pniPreKeys.lastResortKyberPreKeyId < 0) { + Log.i(TAG, "Some last-resort kyber prekeys aren't active yet. Enqueuing a job. ACI: ${SignalStore.account().aciPreKeys.lastResortKyberPreKeyId >= 0} PNI: ${SignalStore.account().pniPreKeys.lastResortKyberPreKeyId >= 0}") + ApplicationDependencies.getJobManager().add(PreKeysSyncJob()) } else { val timeSinceLastFullRefresh = System.currentTimeMillis() - SignalStore.misc().lastFullPrekeyRefreshTime @@ -101,47 +112,101 @@ class PreKeysSyncJob private constructor(parameters: Parameters) : BaseJob(param SignalStore.misc().lastFullPrekeyRefreshTime = System.currentTimeMillis() } - private fun syncPreKeys(serviceIdType: ServiceIdType, serviceId: ServiceId?, protocolStore: SignalProtocolStore, metadataStore: PreKeyMetadataStore) { + private fun syncPreKeys(serviceIdType: ServiceIdType, serviceId: ServiceId?, protocolStore: SignalServiceAccountDataStore, metadataStore: PreKeyMetadataStore) { if (serviceId == null) { warn(TAG, serviceIdType, "AccountId not set!") return } val accountManager = ApplicationDependencies.getSignalServiceAccountManager() + val availablePreKeyCounts: OneTimePreKeyCounts = accountManager.getPreKeyCounts(serviceIdType) - val signedPreKeyRegistered = metadataStore.isSignedPreKeyRegistered && metadataStore.activeSignedPreKeyId >= 0 - val timeSinceLastSignedPreKeyRotation = System.currentTimeMillis() - metadataStore.lastSignedPreKeyRotationTime + val signedPreKeyToUpload: SignedPreKeyRecord? = signedPreKeyUploadIfNeeded(serviceIdType, protocolStore, metadataStore) - val activeSignedPreKeyRecord: SignedPreKeyRecord = if (!signedPreKeyRegistered || timeSinceLastSignedPreKeyRotation >= REFRESH_INTERVAL) { - log(serviceIdType, "Rotating signed prekey. SignedPreKeyRegistered: $signedPreKeyRegistered, TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") - - val signedPreKeyRecord: SignedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore) - accountManager.setSignedPreKey(serviceIdType, signedPreKeyRecord) - - metadataStore.activeSignedPreKeyId = signedPreKeyRecord.id - metadataStore.isSignedPreKeyRegistered = true - metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis() - - signedPreKeyRecord + val oneTimeEcPreKeysToUpload: List? = if (availablePreKeyCounts.ecCount < ONE_TIME_PREKEY_MINIMUM) { + log(serviceIdType, "There are ${availablePreKeyCounts.ecCount} one-time EC prekeys available, which is less than our threshold. Need more.") + PreKeyUtil.generateAndStoreOneTimeEcPreKeys(protocolStore, metadataStore) } else { - log(serviceIdType, "No need to rotate signed prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") - protocolStore.loadSignedPreKey(metadataStore.activeSignedPreKeyId) + log(serviceIdType, "There are ${availablePreKeyCounts.ecCount} one-time EC prekeys available, which is enough.") + null } - val availableOneTimePreKeys = accountManager.getPreKeysCount(serviceIdType) + val lastResortKyberPreKeyToUpload: KyberPreKeyRecord? = lastResortKyberPreKeyUploadIfNeeded(serviceIdType, protocolStore, metadataStore) - if (availableOneTimePreKeys < ONE_TIME_PREKEY_MINIMUM) { - log(serviceIdType, "There are $availableOneTimePreKeys one-time prekeys available, which is not sufficient. Uploading more.") - - val preKeyRecords = PreKeyUtil.generateAndStoreOneTimePreKeys(protocolStore, metadataStore) - val identityKey = protocolStore.identityKeyPair - accountManager.setPreKeys(serviceIdType, identityKey.publicKey, activeSignedPreKeyRecord, preKeyRecords) + val oneTimeKyberPreKeysToUpload: List? = if (availablePreKeyCounts.kyberCount < ONE_TIME_PREKEY_MINIMUM) { + log(serviceIdType, "There are ${availablePreKeyCounts.kyberCount} one-time kyber prekeys available, which is less than our threshold. Need more.") + PreKeyUtil.generateAndStoreOneTimeKyberPreKeys(protocolStore, metadataStore) } else { - log(serviceIdType, "There are $availableOneTimePreKeys one-time prekeys available, which is sufficient. No need to upload.") + log(serviceIdType, "There are ${availablePreKeyCounts.kyberCount} one-time kyber prekeys available, which is enough.") + null + } + + if (signedPreKeyToUpload != null || oneTimeEcPreKeysToUpload != null || lastResortKyberPreKeyToUpload != null || oneTimeKyberPreKeysToUpload != null) { + log(serviceIdType, "Something to upload. SignedPreKey: ${signedPreKeyToUpload != null}, OneTimeEcPreKeys: ${oneTimeEcPreKeysToUpload != null}, LastResortKyberPreKey: ${lastResortKyberPreKeyToUpload != null}, OneTimeKyberPreKeys: ${oneTimeKyberPreKeysToUpload != null}") + accountManager.setPreKeys( + PreKeyUpload( + serviceIdType = serviceIdType, + identityKey = protocolStore.identityKeyPair.publicKey, + signedPreKey = signedPreKeyToUpload, + oneTimeEcPreKeys = oneTimeEcPreKeysToUpload, + lastResortKyberPreKey = lastResortKyberPreKeyToUpload, + oneTimeKyberPreKeys = oneTimeKyberPreKeysToUpload + ) + ) + + if (signedPreKeyToUpload != null) { + log(serviceIdType, "Successfully uploaded signed prekey.") + metadataStore.activeSignedPreKeyId = signedPreKeyToUpload.id + metadataStore.isSignedPreKeyRegistered = true + metadataStore.lastSignedPreKeyRotationTime = System.currentTimeMillis() + } + + if (oneTimeEcPreKeysToUpload != null) { + log(serviceIdType, "Successfully uploaded one-time EC prekeys.") + } + + if (lastResortKyberPreKeyToUpload != null) { + log(serviceIdType, "Successfully uploaded last-resort kyber prekey.") + metadataStore.lastResortKyberPreKeyId = lastResortKyberPreKeyToUpload.id + metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis() + } + + if (oneTimeKyberPreKeysToUpload != null) { + log(serviceIdType, "Successfully uploaded one-time kyber prekeys.") + } + } else { + log(serviceIdType, "No prekeys to upload.") } log(serviceIdType, "Cleaning prekeys...") PreKeyUtil.cleanSignedPreKeys(protocolStore, metadataStore) + PreKeyUtil.cleanLastResortKyberPreKeys(protocolStore, metadataStore) + } + + private fun signedPreKeyUploadIfNeeded(serviceIdType: ServiceIdType, protocolStore: SignalProtocolStore, metadataStore: PreKeyMetadataStore): SignedPreKeyRecord? { + val signedPreKeyRegistered = metadataStore.isSignedPreKeyRegistered && metadataStore.activeSignedPreKeyId >= 0 + val timeSinceLastSignedPreKeyRotation = System.currentTimeMillis() - metadataStore.lastSignedPreKeyRotationTime + + return if (!signedPreKeyRegistered || timeSinceLastSignedPreKeyRotation >= REFRESH_INTERVAL) { + log(serviceIdType, "Rotating signed prekey. SignedPreKeyRegistered: $signedPreKeyRegistered, TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") + PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore) + } else { + log(serviceIdType, "No need to rotate signed prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") + null + } + } + + private fun lastResortKyberPreKeyUploadIfNeeded(serviceIdType: ServiceIdType, protocolStore: SignalServiceAccountDataStore, metadataStore: PreKeyMetadataStore): KyberPreKeyRecord? { + val lastResortRegistered = metadataStore.lastResortKyberPreKeyId >= 0 + val timeSinceLastSignedPreKeyRotation = System.currentTimeMillis() - metadataStore.lastResortKyberPreKeyRotationTime + + return if (!lastResortRegistered || timeSinceLastSignedPreKeyRotation >= REFRESH_INTERVAL) { + log(serviceIdType, "Rotating last-resort kyber prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") + PreKeyUtil.generateAndStoreLastResortKyberPreKey(protocolStore, metadataStore) + } else { + log(serviceIdType, "No need to rotate signed prekey. TimeSinceLastRotation: $timeSinceLastSignedPreKeyRotation ms (${timeSinceLastSignedPreKeyRotation.milliseconds.toDouble(DurationUnit.DAYS)} days)") + null + } } override fun onShouldRetry(e: Exception): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index a170b658c9..5b74927b7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -49,6 +49,9 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private const val KEY_ACI_ACTIVE_SIGNED_PREKEY_ID = "account.aci_active_signed_prekey_id" private const val KEY_ACI_LAST_SIGNED_PREKEY_ROTATION_TIME = "account.aci_last_signed_prekey_rotation_time" private const val KEY_ACI_NEXT_ONE_TIME_PREKEY_ID = "account.aci_next_one_time_prekey_id" + private const val KEY_ACI_NEXT_KYBER_PREKEY_ID = "account.aci_next_kyber_prekey_id" + private const val KEY_ACI_LAST_RESORT_KYBER_PREKEY_ID = "account.aci_last_resort_kyber_prekey_id" + private const val KEY_ACI_LAST_RESORT_KYBER_PREKEY_ROTATION_TIME = "account.aci_last_resort_kyber_prekey_rotation_time" private const val KEY_PNI_IDENTITY_PUBLIC_KEY = "account.pni_identity_public_key" private const val KEY_PNI_IDENTITY_PRIVATE_KEY = "account.pni_identity_private_key" @@ -57,6 +60,9 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private const val KEY_PNI_ACTIVE_SIGNED_PREKEY_ID = "account.pni_active_signed_prekey_id" private const val KEY_PNI_LAST_SIGNED_PREKEY_ROTATION_TIME = "account.pni_last_signed_prekey_rotation_time" private const val KEY_PNI_NEXT_ONE_TIME_PREKEY_ID = "account.pni_next_one_time_prekey_id" + private const val KEY_PNI_NEXT_KYBER_PREKEY_ID = "account.pni_next_kyber_prekey_id" + private const val KEY_PNI_LAST_RESORT_KYBER_PREKEY_ID = "account.pni_last_resort_kyber_prekey_id" + private const val KEY_PNI_LAST_RESORT_KYBER_PREKEY_ROTATION_TIME = "account.pni_last_resort_kyber_prekey_rotation_time" @VisibleForTesting const val KEY_E164 = "account.e164" @@ -258,7 +264,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal override var activeSignedPreKeyId: Int by integerValue(KEY_ACI_ACTIVE_SIGNED_PREKEY_ID, -1) override var isSignedPreKeyRegistered: Boolean by booleanValue(KEY_ACI_SIGNED_PREKEY_REGISTERED, false) override var lastSignedPreKeyRotationTime: Long by longValue(KEY_ACI_LAST_SIGNED_PREKEY_ROTATION_TIME, System.currentTimeMillis() - PreKeysSyncJob.REFRESH_INTERVAL) - override var nextOneTimePreKeyId: Int by integerValue(KEY_ACI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + override var nextEcOneTimePreKeyId: Int by integerValue(KEY_ACI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + override var nextKyberPreKeyId: Int by integerValue(KEY_ACI_NEXT_KYBER_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + override var lastResortKyberPreKeyId: Int by integerValue(KEY_ACI_LAST_RESORT_KYBER_PREKEY_ID, -1) + override var lastResortKyberPreKeyRotationTime: Long by longValue(KEY_ACI_LAST_RESORT_KYBER_PREKEY_ROTATION_TIME, 0) } @get:JvmName("pniPreKeys") @@ -267,7 +276,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal override var activeSignedPreKeyId: Int by integerValue(KEY_PNI_ACTIVE_SIGNED_PREKEY_ID, -1) override var isSignedPreKeyRegistered: Boolean by booleanValue(KEY_PNI_SIGNED_PREKEY_REGISTERED, false) override var lastSignedPreKeyRotationTime: Long by longValue(KEY_PNI_LAST_SIGNED_PREKEY_ROTATION_TIME, System.currentTimeMillis() - PreKeysSyncJob.REFRESH_INTERVAL) - override var nextOneTimePreKeyId: Int by integerValue(KEY_PNI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + override var nextEcOneTimePreKeyId: Int by integerValue(KEY_PNI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + override var nextKyberPreKeyId: Int by integerValue(KEY_PNI_NEXT_KYBER_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + override var lastResortKyberPreKeyId: Int by integerValue(KEY_PNI_LAST_RESORT_KYBER_PREKEY_ID, -1) + override var lastResortKyberPreKeyRotationTime: Long by longValue(KEY_PNI_LAST_RESORT_KYBER_PREKEY_ROTATION_TIME, 0) } /** Indicates whether the user has the ability to receive FCM messages. Largely coupled to whether they have Play Service. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedKyberPreKeyStore.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedKyberPreKeyStore.kt index 19b1646048..9082680219 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedKyberPreKeyStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedKyberPreKeyStore.kt @@ -7,16 +7,16 @@ package org.thoughtcrime.securesms.messages.protocol import org.signal.libsignal.protocol.InvalidKeyIdException import org.signal.libsignal.protocol.state.KyberPreKeyRecord -import org.signal.libsignal.protocol.state.KyberPreKeyStore import org.thoughtcrime.securesms.database.KyberPreKeyTable.KyberPreKey import org.thoughtcrime.securesms.database.SignalDatabase import org.whispersystems.signalservice.api.SignalServiceAccountDataStore +import org.whispersystems.signalservice.api.SignalServiceKyberPreKeyStore import org.whispersystems.signalservice.api.push.ServiceId /** * An in-memory kyber prekey store that is intended to be used temporarily while decrypting messages. */ -class BufferedKyberPreKeyStore(private val selfServiceId: ServiceId) : KyberPreKeyStore { +class BufferedKyberPreKeyStore(private val selfServiceId: ServiceId) : SignalServiceKyberPreKeyStore { /** Our in-memory cache of kyber prekeys. */ val store: MutableMap = mutableMapOf() @@ -46,8 +46,16 @@ class BufferedKyberPreKeyStore(private val selfServiceId: ServiceId) : KyberPreK } } + override fun loadLastResortKyberPreKeys(): List { + error("Not expected in this flow") + } + override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord) { - error("This method is only used in tests") + error("Not expected in this flow") + } + + override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) { + error("Not expected in this flow") } override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean { @@ -67,6 +75,10 @@ class BufferedKyberPreKeyStore(private val selfServiceId: ServiceId) : KyberPreK removedIfNotLastResort += kyberPreKeyId } + override fun removeKyberPreKey(kyberPreKeyId: Int) { + error("Not expected in this flow") + } + fun flushToDisk(persistentStore: SignalServiceAccountDataStore) { for (id in removedIfNotLastResort) { persistentStore.markKyberPreKeyUsed(id) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedSignalServiceAccountDataStore.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedSignalServiceAccountDataStore.kt index 1044a2d074..b875ff88ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedSignalServiceAccountDataStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/protocol/BufferedSignalServiceAccountDataStore.kt @@ -129,6 +129,10 @@ class BufferedSignalServiceAccountDataStore(selfServiceId: ServiceId) : SignalSe kyberPreKeyStore.storeKyberPreKey(kyberPreKeyId, record) } + override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) { + kyberPreKeyStore.storeKyberPreKey(kyberPreKeyId, kyberPreKeyRecord) + } + override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean { return kyberPreKeyStore.containsKyberPreKey(kyberPreKeyId) } @@ -137,6 +141,14 @@ class BufferedSignalServiceAccountDataStore(selfServiceId: ServiceId) : SignalSe return kyberPreKeyStore.markKyberPreKeyUsed(kyberPreKeyId) } + override fun removeKyberPreKey(kyberPreKeyId: Int) { + kyberPreKeyStore.removeKyberPreKey(kyberPreKeyId) + } + + override fun loadLastResortKyberPreKeys(): List { + return kyberPreKeyStore.loadLastResortKyberPreKeys() + } + override fun storeSenderKey(sender: SignalProtocolAddress, distributionId: UUID, record: SenderKeyRecord) { senderKeyStore.storeSenderKey(sender, distributionId, record) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java index 1e7ac3bf05..c0840b86a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; @@ -76,9 +77,9 @@ public class PniAccountInitializationMigrationJob extends MigrationJob { if (!metadataStore.isSignedPreKeyRegistered()) { Log.i(TAG, "Uploading signed prekey for PNI."); SignedPreKeyRecord signedPreKey = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore); - List oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(protocolStore, metadataStore); + List oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(protocolStore, metadataStore); - accountManager.setPreKeys(ServiceIdType.PNI, protocolStore.getIdentityKeyPair().getPublicKey(), signedPreKey, oneTimePreKeys); + accountManager.setPreKeys(new PreKeyUpload(ServiceIdType.PNI, protocolStore.getIdentityKeyPair().getPublicKey(), signedPreKey, oneTimePreKeys, null, null)); metadataStore.setActiveSignedPreKeyId(signedPreKey.getId()); metadataStore.setSignedPreKeyRegistered(true); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java index cc8e486d6c..bc21831e56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -9,6 +9,7 @@ import androidx.annotation.WorkerThread; import androidx.core.app.NotificationManagerCompat; import org.signal.core.util.logging.Log; +import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignalProtocolStore; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; @@ -36,7 +37,9 @@ import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; @@ -185,14 +188,21 @@ public final class RegistrationRepository { private void generateAndRegisterPreKeys(@NonNull ServiceIdType serviceIdType, @NonNull SignalServiceAccountManager accountManager, - @NonNull SignalProtocolStore protocolStore, + @NonNull SignalServiceAccountDataStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) throws IOException { - SignedPreKeyRecord signedPreKey = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore); - List oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(protocolStore, metadataStore); + SignedPreKeyRecord signedPreKey = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore); + List oneTimeEcPreKeys = PreKeyUtil.generateAndStoreOneTimeEcPreKeys(protocolStore, metadataStore); + KyberPreKeyRecord lastResortKyberPreKey = PreKeyUtil.generateAndStoreLastResortKyberPreKey(protocolStore, metadataStore); + List oneTimeKyberPreKeys = PreKeyUtil.generateAndStoreOneTimeKyberPreKeys(protocolStore, metadataStore); - accountManager.setPreKeys(serviceIdType, protocolStore.getIdentityKeyPair().getPublicKey(), signedPreKey, oneTimePreKeys); + accountManager.setPreKeys(new PreKeyUpload(serviceIdType, + protocolStore.getIdentityKeyPair().getPublicKey(), + signedPreKey, + oneTimeEcPreKeys, + lastResortKyberPreKey, + oneTimeKyberPreKeys)); metadataStore.setActiveSignedPreKeyId(signedPreKey.getId()); metadataStore.setSignedPreKeyRegistered(true); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountDataStore.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountDataStore.java index b244e0d309..af5ce04932 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountDataStore.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountDataStore.java @@ -6,7 +6,10 @@ import org.signal.libsignal.protocol.state.SignalProtocolStore; * And extension of the normal protocol store interface that has additional methods that are needed * in the service layer, but not the protocol layer. */ -public interface SignalServiceAccountDataStore extends SignalProtocolStore, SignalServiceSessionStore, SignalServiceSenderKeyStore { +public interface SignalServiceAccountDataStore extends SignalProtocolStore, + SignalServiceSessionStore, + SignalServiceSenderKeyStore, + SignalServiceKyberPreKeyStore { /** * @return True if the user has linked devices, otherwise false. */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 476a5c6311..b3276a1efc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -9,17 +9,16 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; -import org.signal.libsignal.protocol.IdentityKey; 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.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; +import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; @@ -36,7 +35,6 @@ import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceIdType; -import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.exceptions.NoContentException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; @@ -59,6 +57,7 @@ import org.whispersystems.signalservice.internal.push.AuthCredentials; import org.whispersystems.signalservice.internal.push.BackupAuthCheckRequest; import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse; import org.whispersystems.signalservice.internal.push.CdsiAuthResponse; +import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts; import org.whispersystems.signalservice.internal.push.ProfileAvatarData; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse; @@ -80,7 +79,6 @@ import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper import org.whispersystems.util.Base64; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -98,7 +96,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; import javax.annotation.Nullable; @@ -351,23 +348,19 @@ public class SignalServiceAccountManager { * Register an identity key, signed prekey, and list of one time prekeys * with the server. * - * @param identityKey The client's long-term identity keypair. - * @param signedPreKey The client's signed prekey. - * @param oneTimePreKeys The client's list of one-time prekeys. - * * @throws IOException */ - public void setPreKeys(ServiceIdType serviceIdType, IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List oneTimePreKeys) + public void setPreKeys(PreKeyUpload preKeyUpload) throws IOException { - this.pushServiceSocket.registerPreKeys(serviceIdType, identityKey, signedPreKey, oneTimePreKeys); + this.pushServiceSocket.registerPreKeys(preKeyUpload); } /** * @return The server's count of currently available (eg. unused) prekeys for this user. * @throws IOException */ - public int getPreKeysCount(ServiceIdType serviceIdType) throws IOException { + public OneTimePreKeyCounts getPreKeyCounts(ServiceIdType serviceIdType) throws IOException { return this.pushServiceSocket.getAvailablePreKeys(serviceIdType); } @@ -381,14 +374,6 @@ public class SignalServiceAccountManager { this.pushServiceSocket.setCurrentSignedPreKey(serviceIdType, signedPreKey); } - /** - * @return The server's view of the client's current signed prekey. - * @throws IOException - */ - public SignedPreKeyEntity getSignedPreKey(ServiceIdType serviceIdType) throws IOException { - return this.pushServiceSocket.getCurrentSignedPreKey(serviceIdType); - } - /** * @return True if the identifier corresponds to a registered user, otherwise false. */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceKyberPreKeyStore.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceKyberPreKeyStore.kt new file mode 100644 index 0000000000..17aba70e5b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceKyberPreKeyStore.kt @@ -0,0 +1,26 @@ +package org.whispersystems.signalservice.api + +import org.signal.libsignal.protocol.state.KyberPreKeyRecord +import org.signal.libsignal.protocol.state.KyberPreKeyStore + +/** + * And extension of the normal protocol sender key store interface that has additional methods that are + * needed in the service layer, but not the protocol layer. + */ +interface SignalServiceKyberPreKeyStore : KyberPreKeyStore { + + /** + * Identical to [storeKyberPreKey] but indicates that this is a last-resort key rather than a one-time key. + */ + fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) + + /** + * Retrieves all last-resort kyber prekeys. + */ + fun loadLastResortKyberPreKeys(): List + + /** + * Unconditionally remove the specified key from the store. + */ + fun removeKyberPreKey(kyberPreKeyId: Int) +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyUpload.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyUpload.kt new file mode 100644 index 0000000000..b3f39fa821 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/PreKeyUpload.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.account + +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.state.KyberPreKeyRecord +import org.signal.libsignal.protocol.state.PreKeyRecord +import org.signal.libsignal.protocol.state.SignedPreKeyRecord +import org.whispersystems.signalservice.api.push.ServiceIdType + +/** + * Represents a bundle of prekeys you want to upload. + * + * If a field is nullable, not setting it will simply leave that field alone on the service. + */ +data class PreKeyUpload( + val serviceIdType: ServiceIdType, + val identityKey: IdentityKey, + val signedPreKey: SignedPreKeyRecord?, + val oneTimeEcPreKeys: List?, + val lastResortKyberPreKey: KyberPreKeyRecord?, + val oneTimeKyberPreKeys: List? +) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/KyberPreKeyEntity.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/KyberPreKeyEntity.java new file mode 100644 index 0000000000..d123ad4552 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/KyberPreKeyEntity.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ + +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.signal.libsignal.protocol.kem.KEMPublicKey; +import org.whispersystems.util.Base64; + +import java.io.IOException; + +public class KyberPreKeyEntity { + + @JsonProperty + private int keyId; + + @JsonProperty + @JsonSerialize(using = KEMPublicKeySerializer.class) + @JsonDeserialize(using = KEMPublicKeyDeserializer.class) + private KEMPublicKey publicKey; + + @JsonProperty + @JsonSerialize(using = ByteArraySerializer.class) + @JsonDeserialize(using = ByteArrayDeserializer.class) + private byte[] signature; + + public KyberPreKeyEntity() {} + + public KyberPreKeyEntity(int keyId, KEMPublicKey publicKey, byte[] signature) { + this.keyId = keyId; + this.publicKey = publicKey; + this.signature = signature; + } + + public int getKeyId() { + return keyId; + } + + public KEMPublicKey getPublicKey() { + return publicKey; + } + + public byte[] getSignature() { + return signature; + } + + private static class KEMPublicKeySerializer extends JsonSerializer { + @Override + public void serialize(KEMPublicKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize())); + } + } + + private static class KEMPublicKeyDeserializer extends JsonDeserializer { + @Override + public KEMPublicKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return new KEMPublicKey(Base64.decodeWithoutPadding(p.getValueAsString()), 0); + } + } + + private static class ByteArraySerializer extends JsonSerializer { + @Override + public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Base64.encodeBytesWithoutPadding(value)); + } + } + + private static class ByteArrayDeserializer extends JsonDeserializer { + + @Override + public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return Base64.decodeWithoutPadding(p.getValueAsString()); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/OneTimePreKeyCounts.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/OneTimePreKeyCounts.java new file mode 100644 index 0000000000..c26ada1b57 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/OneTimePreKeyCounts.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ + +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OneTimePreKeyCounts { + + @JsonProperty("count") + private int ecCount; + + @JsonProperty("pqCount") + private int kyberCount; + + public OneTimePreKeyCounts() {} + + public OneTimePreKeyCounts(int ecCount, int kyberCount) { + this.ecCount = ecCount; + this.kyberCount = kyberCount; + } + + public int getEcCount() { + return ecCount; + } + + public int getKyberCount() { + return kyberCount; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyResponseItem.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyResponseItem.java index 7c0af5723a..b28066fef3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyResponseItem.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyResponseItem.java @@ -1,6 +1,6 @@ /** * Copyright (C) 2014-2016 Open Whisper Systems - * + *

* Licensed according to the LICENSE file in this repository. */ @@ -13,16 +13,19 @@ import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; public class PreKeyResponseItem { @JsonProperty - public int deviceId; + public int deviceId; @JsonProperty - public int registrationId; + public int registrationId; @JsonProperty public SignedPreKeyEntity signedPreKey; @JsonProperty - public PreKeyEntity preKey; + public PreKeyEntity preKey; + + @JsonProperty("pqPreKey") + public KyberPreKeyEntity kyberPreKey; public int getDeviceId() { return deviceId; @@ -40,4 +43,8 @@ public class PreKeyResponseItem { return preKey; } + public KyberPreKeyEntity getKyberPreKey() { + return kyberPreKey; + } + } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java index 6c51a28dc0..f90f35301e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java @@ -12,23 +12,37 @@ import java.util.List; public class PreKeyState { - @JsonProperty + @JsonProperty("identityKey") @JsonSerialize(using = JsonUtil.IdentityKeySerializer.class) @JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class) private IdentityKey identityKey; - @JsonProperty - private List preKeys; + @JsonProperty("preKeys") + private List oneTimeEcPreKeys; - @JsonProperty + @JsonProperty("signedPreKey") private SignedPreKeyEntity signedPreKey; + @JsonProperty("pqLastResortPreKey") + private KyberPreKeyEntity lastResortKyberKey; + + @JsonProperty("pqPreKeys") + private List oneTimeKyberKeys; + public PreKeyState() {} - public PreKeyState(List preKeys, SignedPreKeyEntity signedPreKey, IdentityKey identityKey) { - this.preKeys = preKeys; - this.signedPreKey = signedPreKey; - this.identityKey = identityKey; + public PreKeyState( + IdentityKey identityKey, + SignedPreKeyEntity signedPreKey, + List oneTimeEcPreKeys, + KyberPreKeyEntity lastResortKyberPreKey, + List oneTimeKyberPreKeys + ) { + this.identityKey = identityKey; + this.signedPreKey = signedPreKey; + this.oneTimeEcPreKeys = oneTimeEcPreKeys; + this.lastResortKyberKey = lastResortKyberPreKey; + this.oneTimeKyberKeys = oneTimeKyberPreKeys; } public IdentityKey getIdentityKey() { @@ -36,7 +50,7 @@ public class PreKeyState { } public List getPreKeys() { - return preKeys; + return oneTimeEcPreKeys; } public SignedPreKeyEntity getSignedPreKey() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyStatus.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyStatus.java deleted file mode 100644 index 9416c42bc0..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyStatus.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.whispersystems.signalservice.internal.push; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class PreKeyStatus { - - @JsonProperty - private int count; - - public PreKeyStatus() {} - - public PreKeyStatus(int count) { - this.count = count; - } - - public int getCount() { - return count; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 57defbdcf5..c0d1d9a83c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -11,12 +11,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; -import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.protocol.kem.KEMPublicKey; import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.protocol.state.PreKeyBundle; -import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.usernames.BaseUsernameException; @@ -41,6 +40,7 @@ import org.signal.storageservice.protos.groups.GroupExternalCredential; import org.signal.storageservice.protos.groups.GroupJoinInfo; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; +import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.groupsv2.CredentialResponse; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; @@ -165,6 +165,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -215,8 +216,8 @@ public class PushServiceSocket { private static final String REQUEST_ACCOUNT_DATA_PATH = "/v2/accounts/data_report"; private static final String PREKEY_METADATA_PATH = "/v2/keys?identity=%s"; - private static final String PREKEY_PATH = "/v2/keys/%s?identity=%s"; - private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s"; + private static final String PREKEY_PATH = "/v2/keys?identity=%s"; + private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s?pq=true"; private static final String SIGNED_PREKEY_PATH = "/v2/keys/signed?identity=%s"; private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code"; @@ -612,67 +613,113 @@ public class PushServiceSocket { makeServiceRequest(String.format(UUID_ACK_MESSAGE_PATH, uuid), "DELETE", null); } - public void registerPreKeys(ServiceIdType serviceIdType, - IdentityKey identityKey, - SignedPreKeyRecord signedPreKey, - List records) + public void registerPreKeys(PreKeyUpload preKeyUpload) throws IOException { - List entities = new LinkedList<>(); + SignedPreKeyEntity signedPreKey = null; + List oneTimeEcPreKeys = null; + KyberPreKeyEntity lastResortKyberPreKey = null; + List oneTimeKyberPreKeys = null; - try { - for (PreKeyRecord record : records) { - PreKeyEntity entity = new PreKeyEntity(record.getId(), - record.getKeyPair().getPublicKey()); - - entities.add(entity); - } - } catch (InvalidKeyException e) { - throw new AssertionError("unexpected invalid key", e); + if (preKeyUpload.getSignedPreKey() != null) { + signedPreKey = new SignedPreKeyEntity(preKeyUpload.getSignedPreKey().getId(), + preKeyUpload.getSignedPreKey().getKeyPair().getPublicKey(), + preKeyUpload.getSignedPreKey().getSignature()); } - SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), - signedPreKey.getKeyPair().getPublicKey(), - signedPreKey.getSignature()); + if (preKeyUpload.getOneTimeEcPreKeys() != null) { + oneTimeEcPreKeys = preKeyUpload + .getOneTimeEcPreKeys() + .stream() + .map(it -> { + try { + return new PreKeyEntity(it.getId(), it.getKeyPair().getPublicKey()); + } catch (InvalidKeyException e) { + throw new AssertionError("unexpected invalid key", e); + } + }) + .collect(Collectors.toList()); + } - makeServiceRequest(String.format(Locale.US, PREKEY_PATH, "", serviceIdType.queryParam()), + if (preKeyUpload.getLastResortKyberPreKey() != null) { + lastResortKyberPreKey = new KyberPreKeyEntity(preKeyUpload.getLastResortKyberPreKey().getId(), + preKeyUpload.getLastResortKyberPreKey().getKeyPair().getPublicKey(), + preKeyUpload.getLastResortKyberPreKey().getSignature()); + } + + if (preKeyUpload.getOneTimeKyberPreKeys() != null) { + oneTimeKyberPreKeys = preKeyUpload + .getOneTimeKyberPreKeys() + .stream() + .map(it -> new KyberPreKeyEntity(it.getId(), it.getKeyPair().getPublicKey(), it.getSignature())) + .collect(Collectors.toList()); + } + + makeServiceRequest(String.format(Locale.US, PREKEY_PATH, preKeyUpload.getServiceIdType().queryParam()), "PUT", - JsonUtil.toJson(new PreKeyState(entities, signedPreKeyEntity, identityKey))); + JsonUtil.toJson(new PreKeyState(preKeyUpload.getIdentityKey(), + signedPreKey, + oneTimeEcPreKeys, + lastResortKyberPreKey, + oneTimeKyberPreKeys))); } - public int getAvailablePreKeys(ServiceIdType serviceIdType) throws IOException { - String path = String.format(PREKEY_METADATA_PATH, serviceIdType.queryParam()); - String responseText = makeServiceRequest(path, "GET", null); - PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class); + public OneTimePreKeyCounts getAvailablePreKeys(ServiceIdType serviceIdType) throws IOException { + String path = String.format(PREKEY_METADATA_PATH, serviceIdType.queryParam()); + String responseText = makeServiceRequest(path, "GET", null); + OneTimePreKeyCounts preKeyStatus = JsonUtil.fromJson(responseText, OneTimePreKeyCounts.class); - return preKeyStatus.getCount(); + return preKeyStatus; } + /** + * Retrieves prekeys. If the specified device is the primary (i.e. deviceId 1), it will retrieve prekeys + * for all devices. If it is not a primary, it will only contain the prekeys for that specific device. + */ public List getPreKeys(SignalServiceAddress destination, Optional unidentifiedAccess, - int deviceIdInteger) + int deviceId) + throws IOException + { + return getPreKeysBySpecifier(destination, unidentifiedAccess, deviceId == 1 ? "*" : String.valueOf(deviceId)); + } + + /** + * Retrieves a prekey for a specific device. + */ + public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException { + List bundles = getPreKeysBySpecifier(destination, Optional.empty(), String.valueOf(deviceId)); + + if (bundles.size() > 0) { + return bundles.get(0); + } else { + throw new IOException("No prekeys available!"); + } + } + + private List getPreKeysBySpecifier(SignalServiceAddress destination, + Optional unidentifiedAccess, + String deviceSpecifier) throws IOException { try { - String deviceId = String.valueOf(deviceIdInteger); + String path = String.format(PREKEY_DEVICE_PATH, destination.getIdentifier(), deviceSpecifier); - if (deviceId.equals("1")) - deviceId = "*"; - - String path = String.format(PREKEY_DEVICE_PATH, destination.getIdentifier(), deviceId); - - Log.d(TAG, "Fetching prekeys for " + destination.getIdentifier() + "." + deviceId + ", i.e. GET " + path); + Log.d(TAG, "Fetching prekeys for " + destination.getIdentifier() + "." + deviceSpecifier + ", i.e. GET " + path); String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, NO_HANDLER, unidentifiedAccess); PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class); List bundles = new LinkedList<>(); for (PreKeyResponseItem device : response.getDevices()) { - ECPublicKey preKey = null; - ECPublicKey signedPreKey = null; - byte[] signedPreKeySignature = null; - int preKeyId = -1; - int signedPreKeyId = -1; + ECPublicKey preKey = null; + ECPublicKey signedPreKey = null; + byte[] signedPreKeySignature = null; + int preKeyId = PreKeyBundle.NULL_PRE_KEY_ID; + int signedPreKeyId = PreKeyBundle.NULL_PRE_KEY_ID; + int kyberPreKeyId = PreKeyBundle.NULL_PRE_KEY_ID; + KEMPublicKey kyberPreKey = null; + byte[] kyberPreKeySignature = null; if (device.getSignedPreKey() != null) { signedPreKey = device.getSignedPreKey().getPublicKey(); @@ -685,9 +732,23 @@ public class PushServiceSocket { preKey = device.getPreKey().getPublicKey(); } - bundles.add(new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, - preKey, signedPreKeyId, signedPreKey, signedPreKeySignature, - response.getIdentityKey())); + if (device.getKyberPreKey() != null) { + kyberPreKey = device.getKyberPreKey().getPublicKey(); + kyberPreKeyId = device.getKyberPreKey().getKeyId(); + kyberPreKeySignature = device.getKyberPreKey().getSignature(); + } + + bundles.add(new PreKeyBundle(device.getRegistrationId(), + device.getDeviceId(), + preKeyId, + preKey, + signedPreKeyId, + signedPreKey, + signedPreKeySignature, + response.getIdentityKey(), + kyberPreKeyId, + kyberPreKey, + kyberPreKeySignature)); } return bundles; @@ -696,52 +757,6 @@ public class PushServiceSocket { } } - public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException { - try { - String path = String.format(PREKEY_DEVICE_PATH, destination.getIdentifier(), String.valueOf(deviceId)); - - String responseText = makeServiceRequest(path, "GET", null); - PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class); - - if (response.getDevices() == null || response.getDevices().size() < 1) - throw new IOException("Empty prekey list"); - - PreKeyResponseItem device = response.getDevices().get(0); - ECPublicKey preKey = null; - ECPublicKey signedPreKey = null; - byte[] signedPreKeySignature = null; - int preKeyId = -1; - int signedPreKeyId = -1; - - if (device.getPreKey() != null) { - preKeyId = device.getPreKey().getKeyId(); - preKey = device.getPreKey().getPublicKey(); - } - - if (device.getSignedPreKey() != null) { - signedPreKeyId = device.getSignedPreKey().getKeyId(); - signedPreKey = device.getSignedPreKey().getPublicKey(); - signedPreKeySignature = device.getSignedPreKey().getSignature(); - } - - return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey, - signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey()); - } catch (NotFoundException nfe) { - throw new UnregisteredUserException(destination.getIdentifier(), nfe); - } - } - - public SignedPreKeyEntity getCurrentSignedPreKey(ServiceIdType serviceIdType) throws IOException { - try { - String path = String.format(SIGNED_PREKEY_PATH, serviceIdType.queryParam()); - String responseText = makeServiceRequest(path, "GET", null); - return JsonUtil.fromJson(responseText, SignedPreKeyEntity.class); - } catch (NotFoundException e) { - Log.w(TAG, e); - return null; - } - } - public void setCurrentSignedPreKey(ServiceIdType serviceIdType, SignedPreKeyRecord signedPreKey) throws IOException { String path = String.format(SIGNED_PREKEY_PATH, serviceIdType.queryParam()); SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), diff --git a/microbenchmark/src/androidTest/java/org/signal/util/InMemorySignalServiceAccountDataStore.kt b/microbenchmark/src/androidTest/java/org/signal/util/InMemorySignalServiceAccountDataStore.kt index 4840508cdc..80545d1d04 100644 --- a/microbenchmark/src/androidTest/java/org/signal/util/InMemorySignalServiceAccountDataStore.kt +++ b/microbenchmark/src/androidTest/java/org/signal/util/InMemorySignalServiceAccountDataStore.kt @@ -5,6 +5,7 @@ import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.SignalProtocolAddress import org.signal.libsignal.protocol.groups.state.SenderKeyRecord import org.signal.libsignal.protocol.state.IdentityKeyStore +import org.signal.libsignal.protocol.state.KyberPreKeyRecord import org.signal.libsignal.protocol.state.PreKeyRecord import org.signal.libsignal.protocol.state.SessionRecord import org.signal.libsignal.protocol.state.SignedPreKeyRecord @@ -19,10 +20,11 @@ class InMemorySignalServiceAccountDataStore : SignalServiceAccountDataStore { private val identityKey: IdentityKeyPair = IdentityKeyPair.generate() private val identities: MutableMap = mutableMapOf() - private val oneTimePreKeys: MutableMap = mutableMapOf() + private val oneTimeEcPreKeys: MutableMap = mutableMapOf() private val signedPreKeys: MutableMap = mutableMapOf() private var sessions: MutableMap = mutableMapOf() private val senderKeys: MutableMap = mutableMapOf() + private val kyberPreKeys: MutableMap = mutableMapOf() override fun getIdentityKeyPair(): IdentityKeyPair { return identityKey @@ -47,19 +49,19 @@ class InMemorySignalServiceAccountDataStore : SignalServiceAccountDataStore { } override fun loadPreKey(preKeyId: Int): PreKeyRecord { - return oneTimePreKeys[preKeyId]!! + return oneTimeEcPreKeys[preKeyId]!! } override fun storePreKey(preKeyId: Int, record: PreKeyRecord) { - oneTimePreKeys[preKeyId] = record + oneTimeEcPreKeys[preKeyId] = record } override fun containsPreKey(preKeyId: Int): Boolean { - return oneTimePreKeys.containsKey(preKeyId) + return oneTimeEcPreKeys.containsKey(preKeyId) } override fun removePreKey(preKeyId: Int) { - oneTimePreKeys.remove(preKeyId) + oneTimeEcPreKeys.remove(preKeyId) } override fun loadSession(address: SignalProtocolAddress): SessionRecord { @@ -120,6 +122,38 @@ class InMemorySignalServiceAccountDataStore : SignalServiceAccountDataStore { return senderKeys[SenderKeyLocator(sender, distributionId)]!! } + override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord { + return kyberPreKeys[kyberPreKeyId]!! + } + + override fun loadKyberPreKeys(): List { + return kyberPreKeys.values.toList() + } + + override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord?) { + error("Not used") + } + + override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean { + return kyberPreKeys.containsKey(kyberPreKeyId) + } + + override fun markKyberPreKeyUsed(kyberPreKeyId: Int) { + kyberPreKeys.remove(kyberPreKeyId) + } + + override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) { + error("Not used") + } + + override fun removeKyberPreKey(kyberPreKeyId: Int) { + error("Not used") + } + + override fun loadLastResortKyberPreKeys(): List { + error("Not used") + } + override fun archiveSession(address: SignalProtocolAddress) { sessions[address]!!.archiveCurrentState() } @@ -137,11 +171,11 @@ class InMemorySignalServiceAccountDataStore : SignalServiceAccountDataStore { } override fun markSenderKeySharedWith(distributionId: DistributionId, addresses: Collection) { - // Not used + // Called, but not needed } override fun clearSenderKeySharedWith(addresses: Collection) { - // Not used + // Called, but not needed } override fun isMultiDevice(): Boolean {