diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 5e3d9aaee1..5240eddfec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -176,7 +176,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializeFcmCheck) - .addNonBlocking(this::initializeSignedPreKeyCheck) + .addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded) .addNonBlocking(this::initializePeriodicTasks) .addNonBlocking(this::initializeCircumvention) .addNonBlocking(this::initializePendingMessages) @@ -352,12 +352,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr } } - private void initializeSignedPreKeyCheck() { - if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) { - ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this)); - } - } - private void initializeExpiringMessageManager() { ApplicationDependencies.getExpiringMessageManager().checkSchedule(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java index 44ee991406..2bb40817c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseCreateActivity.java @@ -63,6 +63,7 @@ public class PassphraseCreateActivity extends PassphraseActivity { MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret); SignalStore.account().generateAciIdentityKey(); + SignalStore.account().generatePniIdentityKey(); VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this); return null; 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 8b452b496d..ca7bdfcdf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -17,64 +17,66 @@ package org.thoughtcrime.securesms.crypto; -import android.content.Context; +import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.IdentityKeyPair; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.PreKeyStore; +import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.libsignal.util.Medium; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; public class PreKeyUtil { @SuppressWarnings("unused") private static final String TAG = Log.tag(PreKeyUtil.class); - private static final int BATCH_SIZE = 100; + 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 List generatePreKeys(Context context) { - PreKeyStore preKeyStore = ApplicationDependencies.getProtocolStore().aci(); List records = new LinkedList<>(); - int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context); + int preKeyIdOffset = metadataStore.getNextOneTimePreKeyId(); - for (int i=0;i allRecords = protocolStore.loadSignedPreKeys(); + + allRecords.stream() + .filter(r -> r.getId() != currentRecord.getId()) + .filter(r -> (now - r.getTimestamp()) > ARCHIVE_AGE) + .sorted(Comparator.comparingLong(SignedPreKeyRecord::getTimestamp).reversed()) + .skip(1) + .forEach(record -> { + Log.i(TAG, "Removing signed prekey record: " + record.getId() + " with timestamp: " + record.getTimestamp()); + protocolStore.removeSignedPreKey(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 new file mode 100644 index 0000000000..82f71c7223 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/PreKeyMetadataStore.kt @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.crypto.storage + +/** + * Allows storing various metadata around prekey state. + */ +interface PreKeyMetadataStore { + var nextSignedPreKeyId: Int + var activeSignedPreKeyId: Int + var isSignedPreKeyRegistered: Boolean + var signedPreKeyFailureCount: Int + var nextOneTimePreKeyId: Int +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java index ecbe2fe11b..29f5d334a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.crypto.storage; -import android.content.Context; - import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; @@ -11,6 +9,7 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; +import org.whispersystems.signalservice.api.push.AccountIdentifier; import java.util.List; @@ -22,16 +21,16 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore { private static final Object LOCK = new Object(); @NonNull - private final Context context; + private final AccountIdentifier accountId; - public TextSecurePreKeyStore(@NonNull Context context) { - this.context = context; + public TextSecurePreKeyStore(@NonNull AccountIdentifier accountId) { + this.accountId = accountId; } @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { synchronized (LOCK) { - PreKeyRecord preKeyRecord = SignalDatabase.preKeys().getPreKey(preKeyId); + PreKeyRecord preKeyRecord = SignalDatabase.oneTimePreKeys().get(accountId, preKeyId); if (preKeyRecord == null) throw new InvalidKeyIdException("No such key: " + preKeyId); else return preKeyRecord; @@ -41,7 +40,7 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore { @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { synchronized (LOCK) { - SignedPreKeyRecord signedPreKeyRecord = SignalDatabase.signedPreKeys().getSignedPreKey(signedPreKeyId); + SignedPreKeyRecord signedPreKeyRecord = SignalDatabase.signedPreKeys().get(accountId, signedPreKeyId); if (signedPreKeyRecord == null) throw new InvalidKeyIdException("No such signed prekey: " + signedPreKeyId); else return signedPreKeyRecord; @@ -51,41 +50,41 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore { @Override public List loadSignedPreKeys() { synchronized (LOCK) { - return SignalDatabase.signedPreKeys().getAllSignedPreKeys(); + return SignalDatabase.signedPreKeys().getAll(accountId); } } @Override public void storePreKey(int preKeyId, PreKeyRecord record) { synchronized (LOCK) { - SignalDatabase.preKeys().insertPreKey(preKeyId, record); + SignalDatabase.oneTimePreKeys().insert(accountId, preKeyId, record); } } @Override public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { synchronized (LOCK) { - SignalDatabase.signedPreKeys().insertSignedPreKey(signedPreKeyId, record); + SignalDatabase.signedPreKeys().insert(accountId, signedPreKeyId, record); } } @Override public boolean containsPreKey(int preKeyId) { - return SignalDatabase.preKeys().getPreKey(preKeyId) != null; + return SignalDatabase.oneTimePreKeys().get(accountId, preKeyId) != null; } @Override public boolean containsSignedPreKey(int signedPreKeyId) { - return SignalDatabase.signedPreKeys().getSignedPreKey(signedPreKeyId) != null; + return SignalDatabase.signedPreKeys().get(accountId, signedPreKeyId) != null; } @Override public void removePreKey(int preKeyId) { - SignalDatabase.preKeys().removePreKey(preKeyId); + SignalDatabase.oneTimePreKeys().delete(accountId, preKeyId); } @Override public void removeSignedPreKey(int signedPreKeyId) { - SignalDatabase.signedPreKeys().removeSignedPreKey(signedPreKeyId); + SignalDatabase.signedPreKeys().delete(accountId, signedPreKeyId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java index e4fc82e92b..786ed52304 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database; import android.app.Application; import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; @@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet; import org.thoughtcrime.securesms.keyvalue.KeyValuePersistentStorage; import org.thoughtcrime.securesms.util.CursorUtil; +import java.io.File; import java.util.Collection; import java.util.Map; @@ -60,6 +62,11 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase return instance; } + public static boolean exists(Context context) { + return context.getDatabasePath(DATABASE_NAME).exists(); + } + + private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) { super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.java deleted file mode 100644 index 1c84ca60a4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.database; - - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.util.Base64; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECKeyPair; -import org.whispersystems.libsignal.ecc.ECPrivateKey; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyRecord; - -import java.io.IOException; - -public class OneTimePreKeyDatabase extends Database { - - private static final String TAG = Log.tag(OneTimePreKeyDatabase.class); - - public static final String TABLE_NAME = "one_time_prekeys"; - private static final String ID = "_id"; - public static final String KEY_ID = "key_id"; - public static final String PUBLIC_KEY = "public_key"; - public static final String PRIVATE_KEY = "private_key"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + - " (" + ID + " INTEGER PRIMARY KEY, " + - KEY_ID + " INTEGER UNIQUE, " + - PUBLIC_KEY + " TEXT NOT NULL, " + - PRIVATE_KEY + " TEXT NOT NULL);"; - - OneTimePreKeyDatabase(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - public @Nullable PreKeyRecord getPreKey(int keyId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?", - new String[] {String.valueOf(keyId)}, - null, null, null)) - { - if (cursor != null && cursor.moveToFirst()) { - try { - ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0); - ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY)))); - - return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey)); - } catch (InvalidKeyException | IOException e) { - Log.w(TAG, e); - } - } - } - - return null; - } - - public void insertPreKey(int keyId, PreKeyRecord record) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - ContentValues contentValues = new ContentValues(); - contentValues.put(KEY_ID, keyId); - contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize())); - contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize())); - - database.replace(TABLE_NAME, null, contentValues); - } - - public void removePreKey(int keyId) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.delete(TABLE_NAME, KEY_ID + " = ?", new String[] {String.valueOf(keyId)}); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.kt new file mode 100644 index 0000000000..c478adf40c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.kt @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.core.content.contentValuesOf +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.SqlUtil +import org.whispersystems.libsignal.InvalidKeyException +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.ecc.ECKeyPair +import org.whispersystems.libsignal.state.PreKeyRecord +import org.whispersystems.signalservice.api.push.AccountIdentifier +import java.io.IOException + +class OneTimePreKeyDatabase internal constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) { + companion object { + private val TAG = Log.tag(OneTimePreKeyDatabase::class.java) + + const val TABLE_NAME = "one_time_prekeys" + const val ID = "_id" + const val ACCOUNT_ID = "account_id" + const val KEY_ID = "key_id" + const val PUBLIC_KEY = "public_key" + const val PRIVATE_KEY = "private_key" + const val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY, + $ACCOUNT_ID TEXT NOT NULL, + $KEY_ID INTEGER UNIQUE, + $PUBLIC_KEY TEXT NOT NULL, + $PRIVATE_KEY TEXT NOT NULL, + UNIQUE($ACCOUNT_ID, $KEY_ID) + ) + """ + } + + fun get(accountId: AccountIdentifier, keyId: Int): PreKeyRecord? { + readableDatabase.query(TABLE_NAME, null, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId), null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + try { + val publicKey = Curve.decodePoint(Base64.decode(cursor.requireNonNullString(PUBLIC_KEY)), 0) + val privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.requireNonNullString(PRIVATE_KEY))) + return PreKeyRecord(keyId, ECKeyPair(publicKey, privateKey)) + } catch (e: InvalidKeyException) { + Log.w(TAG, e) + } catch (e: IOException) { + Log.w(TAG, e) + } + } + } + + return null + } + + fun insert(accountId: AccountIdentifier, keyId: Int, record: PreKeyRecord) { + val contentValues = contentValuesOf( + ACCOUNT_ID to accountId.toString(), + KEY_ID to keyId, + PUBLIC_KEY to Base64.encodeBytes(record.keyPair.publicKey.serialize()), + PRIVATE_KEY to Base64.encodeBytes(record.keyPair.privateKey.serialize()) + ) + + writableDatabase.replace(TABLE_NAME, null, contentValues) + } + + fun delete(accountId: AccountIdentifier, keyId: Int) { + val database = databaseHelper.signalWritableDatabase + database.delete(TABLE_NAME, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId)) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt index 52cabab6c6..879d397469 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt @@ -397,8 +397,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data get() = instance!!.pendingRetryReceiptDatabase @get:JvmStatic - @get:JvmName("preKeys") - val preKeys: OneTimePreKeyDatabase + @get:JvmName("oneTimePreKeys") + val oneTimePreKeys: OneTimePreKeyDatabase get() = instance!!.preKeyDatabase @get:Deprecated("This only exists to migrate from legacy storage. There shouldn't be any new usages.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.java deleted file mode 100644 index 1a3bbc3cd7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.thoughtcrime.securesms.database; - - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.util.Base64; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECKeyPair; -import org.whispersystems.libsignal.ecc.ECPrivateKey; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; - -public class SignedPreKeyDatabase extends Database { - - private static final String TAG = Log.tag(SignedPreKeyDatabase.class); - - public static final String TABLE_NAME = "signed_prekeys"; - - private static final String ID = "_id"; - public static final String KEY_ID = "key_id"; - public static final String PUBLIC_KEY = "public_key"; - public static final String PRIVATE_KEY = "private_key"; - public static final String SIGNATURE = "signature"; - public static final String TIMESTAMP = "timestamp"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + - " (" + ID + " INTEGER PRIMARY KEY, " + - KEY_ID + " INTEGER UNIQUE, " + - PUBLIC_KEY + " TEXT NOT NULL, " + - PRIVATE_KEY + " TEXT NOT NULL, " + - SIGNATURE + " TEXT NOT NULL, " + - TIMESTAMP + " INTEGER DEFAULT 0);"; - - SignedPreKeyDatabase(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - public @Nullable SignedPreKeyRecord getSignedPreKey(int keyId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?", - new String[] {String.valueOf(keyId)}, - null, null, null)) - { - if (cursor != null && cursor.moveToFirst()) { - try { - ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0); - ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY)))); - byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE))); - long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); - - return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature); - } catch (InvalidKeyException | IOException e) { - Log.w(TAG, e); - } - } - } - - return null; - } - - public @NonNull List getAllSignedPreKeys() { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - List results = new LinkedList<>(); - - try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - try { - int keyId = cursor.getInt(cursor.getColumnIndexOrThrow(KEY_ID)); - ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0); - ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY)))); - byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE))); - long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); - - results.add(new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature)); - } catch (InvalidKeyException | IOException e) { - Log.w(TAG, e); - } - } - } - - return results; - } - - public void insertSignedPreKey(int keyId, SignedPreKeyRecord record) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - ContentValues contentValues = new ContentValues(); - contentValues.put(KEY_ID, keyId); - contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize())); - contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize())); - contentValues.put(SIGNATURE, Base64.encodeBytes(record.getSignature())); - contentValues.put(TIMESTAMP, record.getTimestamp()); - - database.replace(TABLE_NAME, null, contentValues); - } - - - public void removeSignedPreKey(int keyId) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.delete(TABLE_NAME, KEY_ID + " = ? AND " + SIGNATURE + " IS NOT NULL", new String[] {String.valueOf(keyId)}); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.kt new file mode 100644 index 0000000000..959917416e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.kt @@ -0,0 +1,99 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.core.content.contentValuesOf +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.SqlUtil +import org.whispersystems.libsignal.InvalidKeyException +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.ecc.ECKeyPair +import org.whispersystems.libsignal.state.SignedPreKeyRecord +import org.whispersystems.signalservice.api.push.AccountIdentifier +import java.io.IOException +import java.util.LinkedList + +class SignedPreKeyDatabase internal constructor(context: Context?, databaseHelper: SignalDatabase?) : Database(context, databaseHelper) { + companion object { + private val TAG = Log.tag(SignedPreKeyDatabase::class.java) + + const val TABLE_NAME = "signed_prekeys" + const val ID = "_id" + const val ACCOUNT_ID = "account_id" + const val KEY_ID = "key_id" + const val PUBLIC_KEY = "public_key" + const val PRIVATE_KEY = "private_key" + const val SIGNATURE = "signature" + const val TIMESTAMP = "timestamp" + const val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY, + $ACCOUNT_ID TEXT NOT NULL, + $KEY_ID INTEGER UNIQUE, + $PUBLIC_KEY TEXT NOT NULL, + $PRIVATE_KEY TEXT NOT NULL, + $SIGNATURE TEXT NOT NULL, + $TIMESTAMP INTEGER DEFAULT 0, + UNIQUE($ACCOUNT_ID, $KEY_ID) + ) + """ + } + + fun get(accountId: AccountIdentifier, keyId: Int): SignedPreKeyRecord? { + readableDatabase.query(TABLE_NAME, null, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId), null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + try { + val publicKey = Curve.decodePoint(Base64.decode(cursor.requireNonNullString(PUBLIC_KEY)), 0) + val privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.requireNonNullString(PRIVATE_KEY))) + val signature = Base64.decode(cursor.requireNonNullString(SIGNATURE)) + val timestamp = cursor.requireLong(TIMESTAMP) + return SignedPreKeyRecord(keyId, timestamp, ECKeyPair(publicKey, privateKey), signature) + } catch (e: InvalidKeyException) { + Log.w(TAG, e) + } catch (e: IOException) { + Log.w(TAG, e) + } + } + } + return null + } + + fun getAll(accountId: AccountIdentifier): List { + val results: MutableList = LinkedList() + + readableDatabase.query(TABLE_NAME, null, "$ACCOUNT_ID = ?", SqlUtil.buildArgs(accountId), null, null, null).use { cursor -> + while (cursor.moveToNext()) { + try { + val keyId = cursor.requireInt(KEY_ID) + val publicKey = Curve.decodePoint(Base64.decode(cursor.requireNonNullString(PUBLIC_KEY)), 0) + val privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.requireNonNullString(PRIVATE_KEY))) + val signature = Base64.decode(cursor.requireNonNullString(SIGNATURE)) + val timestamp = cursor.requireLong(TIMESTAMP) + results.add(SignedPreKeyRecord(keyId, timestamp, ECKeyPair(publicKey, privateKey), signature)) + } catch (e: InvalidKeyException) { + Log.w(TAG, e) + } catch (e: IOException) { + Log.w(TAG, e) + } + } + } + + return results + } + + fun insert(accountId: AccountIdentifier, keyId: Int, record: SignedPreKeyRecord) { + val contentValues = contentValuesOf( + ACCOUNT_ID to accountId.toString(), + KEY_ID to keyId, + PUBLIC_KEY to Base64.encodeBytes(record.keyPair.publicKey.serialize()), + PRIVATE_KEY to Base64.encodeBytes(record.keyPair.privateKey.serialize()), + SIGNATURE to Base64.encodeBytes(record.signature), + TIMESTAMP to record.timestamp + ) + writableDatabase.replace(TABLE_NAME, null, contentValues) + } + + fun delete(accountId: AccountIdentifier, keyId: Int) { + writableDatabase.delete(TABLE_NAME, "$ACCOUNT_ID = ? AND $KEY_ID = ?", SqlUtil.buildArgs(accountId, keyId)) + } +} 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 a49f7aba8e..11e468e70b 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 @@ -14,6 +14,7 @@ import org.signal.core.util.Conversions; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -94,7 +95,7 @@ public final class PreKeyMigrationHelper { reader.close(); Log.i(TAG, "Setting next prekey id: " + index.nextPreKeyId); - TextSecurePreferences.setNextPreKeyId(context, index.nextPreKeyId); + SignalStore.account().aciPreKeys().setNextOneTimePreKeyId(index.nextPreKeyId); } catch (IOException e) { Log.w(TAG, e); } @@ -108,8 +109,8 @@ public final class PreKeyMigrationHelper { Log.i(TAG, "Setting next signed prekey id: " + index.nextSignedPreKeyId); Log.i(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId); - TextSecurePreferences.setNextSignedPreKeyId(context, index.nextSignedPreKeyId); - TextSecurePreferences.setActiveSignedPreKeyId(context, index.activeSignedPreKeyId); + SignalStore.account().aciPreKeys().setNextSignedPreKeyId(index.nextSignedPreKeyId); + SignalStore.account().aciPreKeys().setActiveSignedPreKeyId(index.activeSignedPreKeyId); } catch (IOException e) { Log.w(TAG, e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index e753e98b2c..57e3ede07c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.database.helpers +import android.app.Application import android.app.NotificationChannel import android.content.ContentValues import android.content.Context @@ -18,8 +19,10 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet +import org.thoughtcrime.securesms.database.KeyValueDatabase import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList +import org.thoughtcrime.securesms.database.requireString import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob @@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.util.SqlUtil import org.thoughtcrime.securesms.util.Stopwatch import org.thoughtcrime.securesms.util.Triple import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.DistributionId import java.io.ByteArrayInputStream import java.io.File @@ -185,11 +189,12 @@ object SignalDatabaseMigrations { private const val PNI_CLEANUP = 127 private const val MESSAGE_RANGES = 128 private const val REACTION_TRIGGER_FIX = 129 + private const val PNI_STORES = 130 - const val DATABASE_VERSION = 129 + const val DATABASE_VERSION = 130 @JvmStatic - fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) { db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL") db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.id) @@ -2285,6 +2290,80 @@ object SignalDatabaseMigrations { """.trimIndent() ) } + + if (oldVersion < PNI_STORES) { + val localAci: ACI? = getLocalAci(context) + + // One-Time Prekeys + db.execSQL( + """ + CREATE TABLE one_time_prekeys_tmp ( + _id INTEGER PRIMARY KEY, + account_id TEXT NOT NULL, + key_id INTEGER, + public_key TEXT NOT NULL, + private_key TEXT NOT NULL, + UNIQUE(account_id, key_id) + ) + """.trimIndent() + ) + + if (localAci != null) { + db.execSQL( + """ + INSERT INTO one_time_prekeys_tmp (account_id, key_id, public_key, private_key) + SELECT + '$localAci' AS account_id, + one_time_prekeys.key_id, + one_time_prekeys.public_key, + one_time_prekeys.private_key + FROM one_time_prekeys + """.trimIndent() + ) + } else { + Log.w(TAG, "No local ACI set. Not migrating any existing one-time prekeys.") + } + + db.execSQL("DROP TABLE one_time_prekeys") + db.execSQL("ALTER TABLE one_time_prekeys_tmp RENAME TO one_time_prekeys") + + // Signed Prekeys + db.execSQL( + """ + CREATE TABLE signed_prekeys_tmp ( + _id INTEGER PRIMARY KEY, + account_id TEXT NOT NULL, + key_id INTEGER, + public_key TEXT NOT NULL, + private_key TEXT NOT NULL, + signature TEXT NOT NULL, + timestamp INTEGER DEFAULT 0, + UNIQUE(account_id, key_id) + ) + """.trimIndent() + ) + + if (localAci != null) { + db.execSQL( + """ + INSERT INTO signed_prekeys_tmp (account_id, key_id, public_key, private_key, signature, timestamp) + SELECT + '$localAci' AS account_id, + signed_prekeys.key_id, + signed_prekeys.public_key, + signed_prekeys.private_key, + signed_prekeys.signature, + signed_prekeys.timestamp + FROM signed_prekeys + """.trimIndent() + ) + } else { + Log.w(TAG, "No local ACI set. Not migrating any existing signed prekeys.") + } + + db.execSQL("DROP TABLE signed_prekeys") + db.execSQL("ALTER TABLE signed_prekeys_tmp RENAME TO signed_prekeys") + } } @JvmStatic @@ -2294,6 +2373,9 @@ object SignalDatabaseMigrations { } } + /** + * Important: You can't change this method, or you risk breaking existing migrations. If you need to change this, make a new method. + */ private fun migrateReaction(db: SQLiteDatabase, cursor: Cursor, isMms: Boolean) { try { val messageId = CursorUtil.requireLong(cursor, "_id") @@ -2314,4 +2396,22 @@ object SignalDatabaseMigrations { Log.w(TAG, "Failed to parse reaction!") } } + + /** + * Important: You can't change this method, or you risk breaking existing migrations. If you need to change this, make a new method. + */ + private fun getLocalAci(context: Application): ACI? { + if (KeyValueDatabase.exists(context)) { + val keyValueDatabase = KeyValueDatabase.getInstance(context).readableDatabase + keyValueDatabase.query("key_value", arrayOf("value"), "key = ?", SqlUtil.buildArgs("account.aci"), null, null, null).use { cursor -> + return if (cursor.moveToFirst()) { + ACI.parseOrNull(cursor.requireString("value")) + } else { + null + } + } + } else { + return ACI.parseOrNull(PreferenceManager.getDefaultSharedPreferences(context).getString("pref_local_uuid", null)) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 07f4959de8..f3d83465d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -278,16 +278,27 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr @Override public @NonNull SignalServiceDataStoreImpl provideProtocolStore() { + ACI localAci = SignalStore.account().getAci(); + PNI localPni = SignalStore.account().getPni(); + + if (localAci == null) { + throw new IllegalStateException("No ACI set!"); + } + + if (localPni == null) { + throw new IllegalStateException("No PNI set!"); + } + SignalBaseIdentityKeyStore baseIdentityStore = new SignalBaseIdentityKeyStore(context); SignalServiceAccountDataStoreImpl aciStore = new SignalServiceAccountDataStoreImpl(context, - new TextSecurePreKeyStore(context), + new TextSecurePreKeyStore(localAci), new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getAciIdentityKey()), new TextSecureSessionStore(context), new SignalSenderKeyStore(context)); SignalServiceAccountDataStoreImpl pniStore = new SignalServiceAccountDataStoreImpl(context, - new TextSecurePreKeyStore(context), + new TextSecurePreKeyStore(localPni), new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getPniIdentityKey()), new TextSecureSessionStore(context), new SignalSenderKeyStore(context)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java index 19e7f22967..520581a3f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CleanPreKeysJob.java @@ -4,37 +4,24 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyStore; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; - -import java.io.IOException; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.whispersystems.libsignal.state.SignalProtocolStore; +/** + * Deprecated. Only exists for previously-enqueued jobs. + * Use {@link PreKeyUtil#cleanSignedPreKeys(SignalProtocolStore, PreKeyMetadataStore)} instead. + */ +@Deprecated public class CleanPreKeysJob extends BaseJob { public static final String KEY = "CleanPreKeysJob"; private static final String TAG = Log.tag(CleanPreKeysJob.class); - private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30); - - public CleanPreKeysJob() { - this(new Job.Parameters.Builder() - .setQueue("CleanPreKeysJob") - .setMaxAttempts(5) - .build()); - } - private CleanPreKeysJob(@NonNull Job.Parameters parameters) { super(parameters); } @@ -50,47 +37,13 @@ public class CleanPreKeysJob extends BaseJob { } @Override - public void onRun() throws IOException { - try { - Log.i(TAG, "Cleaning prekeys..."); - - int activeSignedPreKeyId = PreKeyUtil.getActiveSignedPreKeyId(context); - SignedPreKeyStore signedPreKeyStore = ApplicationDependencies.getProtocolStore().aci(); - - if (activeSignedPreKeyId < 0) return; - - SignedPreKeyRecord currentRecord = signedPreKeyStore.loadSignedPreKey(activeSignedPreKeyId); - List allRecords = signedPreKeyStore.loadSignedPreKeys(); - LinkedList oldRecords = removeRecordFrom(currentRecord, allRecords); - - Collections.sort(oldRecords, new SignedPreKeySorter()); - - Log.i(TAG, "Active signed prekey: " + activeSignedPreKeyId); - Log.i(TAG, "Old signed prekey record count: " + oldRecords.size()); - - boolean foundAgedRecord = false; - - for (SignedPreKeyRecord oldRecord : oldRecords) { - long archiveDuration = System.currentTimeMillis() - oldRecord.getTimestamp(); - - if (archiveDuration >= ARCHIVE_AGE) { - if (!foundAgedRecord) { - foundAgedRecord = true; - } else { - Log.i(TAG, "Removing signed prekey record: " + oldRecord.getId() + " with timestamp: " + oldRecord.getTimestamp()); - signedPreKeyStore.removeSignedPreKey(oldRecord.getId()); - } - } - } - } catch (InvalidKeyIdException e) { - Log.w(TAG, e); - } + public void onRun() { + PreKeyUtil.cleanSignedPreKeys(ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys()); + PreKeyUtil.cleanSignedPreKeys(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys()); } @Override public boolean onShouldRetry(@NonNull Exception throwable) { - if (throwable instanceof NonSuccessfulResponseCodeException) return false; - if (throwable instanceof PushNetworkException) return true; return false; } @@ -99,30 +52,6 @@ public class CleanPreKeysJob extends BaseJob { Log.w(TAG, "Failed to execute clean signed prekeys task."); } - private LinkedList removeRecordFrom(SignedPreKeyRecord currentRecord, - List records) - - { - LinkedList others = new LinkedList<>(); - - for (SignedPreKeyRecord record : records) { - if (record.getId() != currentRecord.getId()) { - others.add(record); - } - } - - return others; - } - - private static class SignedPreKeySorter implements Comparator { - @Override - public int compare(SignedPreKeyRecord lhs, SignedPreKeyRecord rhs) { - if (lhs.getTimestamp() > rhs.getTimestamp()) return -1; - else if (lhs.getTimestamp() < rhs.getTimestamp()) return 1; - else return 0; - } - } - public static final class Factory implements Job.Factory { @Override public @NonNull CleanPreKeysJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java index 9f82487958..c26063f2c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateSignedPreKeyJob.java @@ -1,36 +1,41 @@ package org.thoughtcrime.securesms.jobs; -import android.content.Context; - import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.AccountIdentifier; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; +import java.util.concurrent.TimeUnit; +/** + * Creates and uploads a new signed prekey for an identity if one hasn't been uploaded yet. + */ public class CreateSignedPreKeyJob extends BaseJob { public static final String KEY = "CreateSignedPreKeyJob"; private static final String TAG = Log.tag(CreateSignedPreKeyJob.class); - public CreateSignedPreKeyJob(Context context) { + private CreateSignedPreKeyJob() { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) + .setMaxInstancesForFactory(1) .setQueue("CreateSignedPreKeyJob") - .setMaxAttempts(25) + .setLifespan(TimeUnit.DAYS.toMillis(30)) + .setMaxAttempts(Parameters.UNLIMITED) .build()); } @@ -38,6 +43,16 @@ public class CreateSignedPreKeyJob extends BaseJob { super(parameters); } + /** + * Enqueues an instance of this job if we not yet created + uploaded signed prekeys for one of our identities. + */ + public static void enqueueIfNeeded() { + if (!SignalStore.account().aciPreKeys().isSignedPreKeyRegistered() || !SignalStore.account().pniPreKeys().isSignedPreKeyRegistered()) { + Log.i(TAG, "Some signed prekeys aren't registered yet. Enqueuing a job."); + ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob()); + } + } + @Override public @NonNull Data serialize() { return Data.EMPTY; @@ -50,22 +65,33 @@ public class CreateSignedPreKeyJob extends BaseJob { @Override public void onRun() throws IOException { - if (TextSecurePreferences.isSignedPreKeyRegistered(context)) { - Log.w(TAG, "Signed prekey already registered..."); - return; - } - if (!SignalStore.account().isRegistered()) { Log.w(TAG, "Not yet registered..."); return; } - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey(); - SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKeyPair, true); + createPreKeys(SignalStore.account().getAci(), ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys()); + createPreKeys(SignalStore.account().getPni(), ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys()); + } - accountManager.setSignedPreKey(signedPreKeyRecord); - TextSecurePreferences.setSignedPreKeyRegistered(context, true); + private void createPreKeys(@Nullable AccountIdentifier accountId, @NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) + throws IOException + { + if (accountId == null) { + warn(TAG, "AccountId not set!"); + return; + } + + if (metadataStore.isSignedPreKeyRegistered()) { + warn(TAG, "Signed prekey for " + (accountId.isAci() ? "ACI" : "PNI") + " already registered..."); + return; + } + + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, true); + + accountManager.setSignedPreKey(accountId, signedPreKeyRecord); + metadataStore.setSignedPreKeyRegistered(true); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index a6e1f85d7c..2338ba1081 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.migrations.MigrationCompleteJob; import org.thoughtcrime.securesms.migrations.PassingMigrationJob; import org.thoughtcrime.securesms.migrations.PinOptOutMigration; import org.thoughtcrime.securesms.migrations.PinReminderMigrationJob; +import org.thoughtcrime.securesms.migrations.PniAccountInitializationMigrationJob; import org.thoughtcrime.securesms.migrations.PniMigrationJob; import org.thoughtcrime.securesms.migrations.ProfileMigrationJob; import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob; @@ -197,6 +198,7 @@ public final class JobManagerFactories { put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory()); put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory()); put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory()); + put(PniAccountInitializationMigrationJob.KEY, new PniAccountInitializationMigrationJob.Factory()); put(PniMigrationJob.KEY, new PniMigrationJob.Factory()); put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory()); put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 6ff43706d5..342c46c646 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -100,7 +100,7 @@ public abstract class PushSendJob extends SendJob { @Override protected final void onSend() throws Exception { - if (TextSecurePreferences.getSignedPreKeyFailureCount(context) > 5) { + if (SignalStore.account().aciPreKeys().getSignedPreKeyFailureCount() > 5) { ApplicationDependencies.getJobManager().add(new RotateSignedPreKeyJob()); throw new TextSecureExpiredException("Too many signed prekey rotation failures"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java index 728c154ddf..d838219a6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshPreKeysJob.java @@ -1,20 +1,24 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.AccountIdentifier; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -22,6 +26,11 @@ import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; +/** + * Ensures that our prekeys are up to date for both our ACI and PNI identities. + * Specifically, if we have less than {@link #PREKEY_MINIMUM} one-time prekeys, we will generate and upload + * a new batch of one-time prekeys, as well as a new signed prekey. + */ public class RefreshPreKeysJob extends BaseJob { public static final String KEY = "RefreshPreKeysJob"; @@ -72,34 +81,61 @@ public class RefreshPreKeysJob extends BaseJob { return; } - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + ACI aci = SignalStore.account().getAci(); + SignalProtocolStore aciProtocolStore = ApplicationDependencies.getProtocolStore().aci(); + PreKeyMetadataStore aciPreKeyStore = SignalStore.account().aciPreKeys(); + + PNI pni = SignalStore.account().getPni(); + SignalProtocolStore pniProtocolStore = ApplicationDependencies.getProtocolStore().pni(); + PreKeyMetadataStore pniPreKeyStore = SignalStore.account().pniPreKeys(); - int availableKeys = accountManager.getPreKeysCount(); - - Log.i(TAG, "Available keys: " + availableKeys); - - if (availableKeys >= PREKEY_MINIMUM && TextSecurePreferences.isSignedPreKeyRegistered(context)) { - Log.i(TAG, "Available keys sufficient."); - SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis()); - return; + if (refreshKeys(aci, aciProtocolStore, aciPreKeyStore)) { + PreKeyUtil.cleanSignedPreKeys(aciProtocolStore, aciPreKeyStore); + } + + if (refreshKeys(pni, pniProtocolStore, pniPreKeyStore)) { + PreKeyUtil.cleanSignedPreKeys(pniProtocolStore, pniPreKeyStore); } - List preKeyRecords = PreKeyUtil.generatePreKeys(context); - IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); - SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false); - - Log.i(TAG, "Registering new prekeys..."); - - accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKeyRecord, preKeyRecords); - - PreKeyUtil.setActiveSignedPreKeyId(context, signedPreKeyRecord.getId()); - TextSecurePreferences.setSignedPreKeyRegistered(context, true); - - ApplicationDependencies.getJobManager().add(new CleanPreKeysJob()); SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis()); Log.i(TAG, "Successfully refreshed prekeys."); } + /** + * @return True if we need to clean prekeys, otherwise false. + */ + private boolean refreshKeys(@Nullable AccountIdentifier accountId, @NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) throws IOException { + if (accountId == null) { + throw new IOException("Unset identifier!"); + } + + String logPrefix = "[" + (accountId.isAci() ? "ACI" : "PNI") + "] "; + + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + + int availableKeys = accountManager.getPreKeysCount(accountId); + log(TAG, logPrefix + "Available keys: " + availableKeys); + + if (availableKeys >= PREKEY_MINIMUM && metadataStore.isSignedPreKeyRegistered()) { + log(TAG, logPrefix + "Available keys sufficient."); + return false; + } + + List preKeyRecords = PreKeyUtil.generateAndStoreOneTimePreKeys(protocolStore, metadataStore); + SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, false); + IdentityKeyPair identityKey = protocolStore.getIdentityKeyPair(); + + log(TAG, logPrefix + "Registering new prekeys..."); + + accountManager.setPreKeys(accountId, identityKey.getPublicKey(), signedPreKeyRecord, preKeyRecords); + + metadataStore.setActiveSignedPreKeyId(signedPreKeyRecord.getId()); + metadataStore.setSignedPreKeyRegistered(true); + + log(TAG, logPrefix + "Need to clean prekeys."); + return true; + } + @Override public boolean onShouldRetry(@NonNull Exception exception) { if (exception instanceof NonSuccessfulResponseCodeException) return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java index f1b64539c9..1e0fd97243 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateSignedPreKeyJob.java @@ -4,21 +4,28 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.AccountIdentifier; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import java.io.IOException; import java.util.concurrent.TimeUnit; +/** + * Forces the creation + upload of new signed prekeys for both the ACI and PNI identities. + */ public class RotateSignedPreKeyJob extends BaseJob { public static final String KEY = "RotateSignedPreKeyJob"; @@ -53,17 +60,34 @@ public class RotateSignedPreKeyJob extends BaseJob { public void onRun() throws Exception { Log.i(TAG, "Rotating signed prekey..."); + ACI aci = SignalStore.account().getAci(); + PNI pni = SignalStore.account().getPni(); + + if (aci == null) { + Log.w(TAG, "ACI is unset!"); + return; + } + + if (pni == null) { + Log.w(TAG, "PNI is unset!"); + return; + } + + rotate(aci, ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys()); + rotate(pni, ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys()); + } + + private void rotate(@NonNull AccountIdentifier accountId, @NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) + throws IOException + { SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); - SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false); + SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, false); - accountManager.setSignedPreKey(signedPreKeyRecord); + accountManager.setSignedPreKey(accountId, signedPreKeyRecord); - PreKeyUtil.setActiveSignedPreKeyId(context, signedPreKeyRecord.getId()); - TextSecurePreferences.setSignedPreKeyRegistered(context, true); - TextSecurePreferences.setSignedPreKeyFailureCount(context, 0); - - ApplicationDependencies.getJobManager().add(new CleanPreKeysJob()); + metadataStore.setActiveSignedPreKeyId(signedPreKeyRecord.getId()); + metadataStore.setSignedPreKeyRegistered(true); + metadataStore.setSignedPreKeyFailureCount(0); } @Override @@ -73,7 +97,11 @@ public class RotateSignedPreKeyJob extends BaseJob { @Override public void onFailure() { - TextSecurePreferences.setSignedPreKeyFailureCount(context, TextSecurePreferences.getSignedPreKeyFailureCount(context) + 1); + PreKeyMetadataStore aciStore = SignalStore.account().aciPreKeys(); + PreKeyMetadataStore pniStore = SignalStore.account().pniPreKeys(); + + aciStore.setSignedPreKeyFailureCount(aciStore.getSignedPreKeyFailureCount() + 1); + pniStore.setSignedPreKeyFailureCount(pniStore.getSignedPreKeyFailureCount() + 1); } public static final class Factory implements Job.Factory { 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 466b16d41e..bd5406938e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MasterCipher import org.thoughtcrime.securesms.crypto.ProfileKeyUtil +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient @@ -18,9 +19,11 @@ import org.thoughtcrime.securesms.util.Util import org.whispersystems.libsignal.IdentityKey import org.whispersystems.libsignal.IdentityKeyPair import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.util.Medium import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.SignalServiceAddress +import java.security.SecureRandom internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) { @@ -35,10 +38,22 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal private const val KEY_FCM_TOKEN_LAST_SET_TIME = "account.fcm_token_last_set_time" private const val KEY_DEVICE_NAME = "account.device_name" private const val KEY_DEVICE_ID = "account.device_id" + private const val KEY_ACI_IDENTITY_PUBLIC_KEY = "account.aci_identity_public_key" private const val KEY_ACI_IDENTITY_PRIVATE_KEY = "account.aci_identity_private_key" + private const val KEY_ACI_SIGNED_PREKEY_REGISTERED = "account.aci_signed_prekey_registered" + private const val KEY_ACI_NEXT_SIGNED_PREKEY_ID = "account.aci_next_signed_prekey_id" + private const val KEY_ACI_ACTIVE_SIGNED_PREKEY_ID = "account.aci_active_signed_prekey_id" + private const val KEY_ACI_SIGNED_PREKEY_FAILURE_COUNT = "account.aci_signed_prekey_failure_count" + private const val KEY_ACI_NEXT_ONE_TIME_PREKEY_ID = "account.aci_next_one_time_prekey_id" + 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" + private const val KEY_PNI_SIGNED_PREKEY_REGISTERED = "account.pni_signed_prekey_registered" + private const val KEY_PNI_NEXT_SIGNED_PREKEY_ID = "account.pni_next_signed_prekey_id" + private const val KEY_PNI_ACTIVE_SIGNED_PREKEY_ID = "account.pni_active_signed_prekey_id" + private const val KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT = "account.pni_signed_prekey_failure_count" + private const val KEY_PNI_NEXT_ONE_TIME_PREKEY_ID = "account.pni_next_one_time_prekey_id" @VisibleForTesting const val KEY_E164 = "account.e164" @@ -102,9 +117,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal } /** A randomly-generated value that represents this registration instance. Helps the server know if you reinstalled. */ - var registrationId: Int - get() = getInteger(KEY_REGISTRATION_ID, 0) - set(value) = putInteger(KEY_REGISTRATION_ID, value) + var registrationId: Int by integerValue(KEY_REGISTRATION_ID, 0) /** The identity key pair for the ACI identity. */ val aciIdentityKey: IdentityKeyPair @@ -147,8 +160,8 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal val key: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() store .beginWrite() - .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize()) - .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize()) + .putBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize()) + .putBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize()) .commit() } @@ -174,11 +187,27 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, Base64.decode(base64)) } + @get:JvmName("aciPreKeys") + val aciPreKeys: PreKeyMetadataStore = object : PreKeyMetadataStore { + override var nextSignedPreKeyId: Int by integerValue(KEY_ACI_NEXT_SIGNED_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + 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 signedPreKeyFailureCount: Int by integerValue(KEY_ACI_SIGNED_PREKEY_FAILURE_COUNT, 0) + override var nextOneTimePreKeyId: Int by integerValue(KEY_ACI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + } + + @get:JvmName("pniPreKeys") + val pniPreKeys: PreKeyMetadataStore = object : PreKeyMetadataStore { + override var nextSignedPreKeyId: Int by integerValue(KEY_PNI_NEXT_SIGNED_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + 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 signedPreKeyFailureCount: Int by integerValue(KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT, 0) + override var nextOneTimePreKeyId: Int by integerValue(KEY_PNI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + } + /** Indicates whether the user has the ability to receive FCM messages. Largely coupled to whether they have Play Service. */ - var fcmEnabled: Boolean - @JvmName("isFcmEnabled") - get() = getBoolean(KEY_FCM_ENABLED, false) - set(value) = putBoolean(KEY_FCM_ENABLED, value) + @get:JvmName("isFcmEnabled") + var fcmEnabled: Boolean by booleanValue(KEY_FCM_ENABLED, false) /** The FCM token, which allows the server to send us FCM messages. */ var fcmToken: String? @@ -249,6 +278,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal ApplicationDependencies.getGroupsV2Authorization().clear() } + /** Do not alter. If you need to migrate more stuff, create a new method. */ private fun migrateFromSharedPrefsV1(context: Context) { Log.i(TAG, "[V1] Migrating account values from shared prefs.") @@ -263,23 +293,24 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal putLong(KEY_FCM_TOKEN_LAST_SET_TIME, TextSecurePreferences.getLongPreference(context, "pref_gcm_registration_id_last_set_time", 0)) } + /** Do not alter. If you need to migrate more stuff, create a new method. */ private fun migrateFromSharedPrefsV2(context: Context) { Log.i(TAG, "[V2] Migrating account values from shared prefs.") val masterSecretPrefs: SharedPreferences = context.getSharedPreferences("SecureSMS-Preferences", 0) val defaultPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val storeWriter: KeyValueStore.Writer = store.beginWrite() + if (masterSecretPrefs.hasStringData("pref_identity_public_v3")) { Log.i(TAG, "Migrating modern identity key.") val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_v3", null)!!) val identityPrivate = Base64.decode(masterSecretPrefs.getString("pref_identity_private_v3", null)!!) - store - .beginWrite() + storeWriter .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic) .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate) - .commit() } else if (masterSecretPrefs.hasStringData("pref_identity_public_curve25519")) { Log.i(TAG, "Migrating legacy identity key.") @@ -287,15 +318,21 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_curve25519", null)!!) val identityPrivate = masterCipher.decryptKey(Base64.decode(masterSecretPrefs.getString("pref_identity_private_curve25519", null)!!)).serialize() - store - .beginWrite() + storeWriter .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic) .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate) - .commit() } else { Log.w(TAG, "No pre-existing identity key! No migration.") } + storeWriter + .putInteger(KEY_ACI_NEXT_SIGNED_PREKEY_ID, defaultPrefs.getInt("pref_next_signed_pre_key_id", SecureRandom().nextInt(Medium.MAX_VALUE))) + .putInteger(KEY_ACI_ACTIVE_SIGNED_PREKEY_ID, defaultPrefs.getInt("pref_active_signed_pre_key_id", -1)) + .putInteger(KEY_ACI_NEXT_ONE_TIME_PREKEY_ID, defaultPrefs.getInt("pref_next_pre_key_id", SecureRandom().nextInt(Medium.MAX_VALUE))) + .putInteger(KEY_ACI_SIGNED_PREKEY_FAILURE_COUNT, defaultPrefs.getInt("pref_signed_prekey_failure_count", 0)) + .putBoolean(KEY_ACI_SIGNED_PREKEY_REGISTERED, defaultPrefs.getBoolean("pref_signed_prekey_registered", false)) + .commit() + masterSecretPrefs .edit() .remove("pref_identity_public_v3") @@ -308,6 +345,11 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal .edit() .remove("pref_local_uuid") .remove("pref_identity_public_v3") + .remove("pref_next_signed_pre_key_id") + .remove("pref_active_signed_pre_key_id") + .remove("pref_signed_prekey_failure_count") + .remove("pref_signed_prekey_registered") + .remove("pref_next_pre_key_id") .remove("pref_gcm_password") .remove("pref_gcm_registered") .remove("pref_local_registration_id") diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValueDelegates.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValueDelegates.kt index 269db8954a..a9360cd695 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValueDelegates.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValueDelegates.kt @@ -3,102 +3,102 @@ package org.thoughtcrime.securesms.keyvalue import kotlin.reflect.KProperty internal fun SignalStoreValues.longValue(key: String, default: Long): SignalStoreValueDelegate { - return LongValue(key, default) + return LongValue(key, default, this.store) } internal fun SignalStoreValues.booleanValue(key: String, default: Boolean): SignalStoreValueDelegate { - return BooleanValue(key, default) + return BooleanValue(key, default, this.store) } internal fun SignalStoreValues.stringValue(key: String, default: T): SignalStoreValueDelegate { - return StringValue(key, default) + return StringValue(key, default, this.store) } internal fun SignalStoreValues.integerValue(key: String, default: Int): SignalStoreValueDelegate { - return IntValue(key, default) + return IntValue(key, default, this.store) } internal fun SignalStoreValues.floatValue(key: String, default: Float): SignalStoreValueDelegate { - return FloatValue(key, default) + return FloatValue(key, default, this.store) } internal fun SignalStoreValues.blobValue(key: String, default: ByteArray): SignalStoreValueDelegate { - return BlobValue(key, default) + return BlobValue(key, default, this.store) } /** * Kotlin delegate that serves as a base for all other value types. This allows us to only expose this sealed * class to callers and protect the individual implementations as private behind the various extension functions. */ -sealed class SignalStoreValueDelegate { +sealed class SignalStoreValueDelegate(private val store: KeyValueStore) { operator fun getValue(thisRef: Any?, property: KProperty<*>): T { - return getValue(thisRef as SignalStoreValues) + return getValue(store) } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - setValue(thisRef as SignalStoreValues, value) + setValue(store, value) } - internal abstract fun getValue(values: SignalStoreValues): T - internal abstract fun setValue(values: SignalStoreValues, value: T) + internal abstract fun getValue(values: KeyValueStore): T + internal abstract fun setValue(values: KeyValueStore, value: T) } -private class LongValue(private val key: String, private val default: Long) : SignalStoreValueDelegate() { - override fun getValue(values: SignalStoreValues): Long { +private class LongValue(private val key: String, private val default: Long, store: KeyValueStore) : SignalStoreValueDelegate(store) { + override fun getValue(values: KeyValueStore): Long { return values.getLong(key, default) } - override fun setValue(values: SignalStoreValues, value: Long) { - values.putLong(key, value) + override fun setValue(values: KeyValueStore, value: Long) { + values.beginWrite().putLong(key, value).apply() } } -private class BooleanValue(private val key: String, private val default: Boolean) : SignalStoreValueDelegate() { - override fun getValue(values: SignalStoreValues): Boolean { +private class BooleanValue(private val key: String, private val default: Boolean, store: KeyValueStore) : SignalStoreValueDelegate(store) { + override fun getValue(values: KeyValueStore): Boolean { return values.getBoolean(key, default) } - override fun setValue(values: SignalStoreValues, value: Boolean) { - values.putBoolean(key, value) + override fun setValue(values: KeyValueStore, value: Boolean) { + values.beginWrite().putBoolean(key, value).apply() } } -private class StringValue(private val key: String, private val default: T) : SignalStoreValueDelegate() { - override fun getValue(values: SignalStoreValues): T { +private class StringValue(private val key: String, private val default: T, store: KeyValueStore) : SignalStoreValueDelegate(store) { + override fun getValue(values: KeyValueStore): T { return values.getString(key, default) as T } - override fun setValue(values: SignalStoreValues, value: T) { - values.putString(key, value) + override fun setValue(values: KeyValueStore, value: T) { + values.beginWrite().putString(key, value).apply() } } -private class IntValue(private val key: String, private val default: Int) : SignalStoreValueDelegate() { - override fun getValue(values: SignalStoreValues): Int { +private class IntValue(private val key: String, private val default: Int, store: KeyValueStore) : SignalStoreValueDelegate(store) { + override fun getValue(values: KeyValueStore): Int { return values.getInteger(key, default) } - override fun setValue(values: SignalStoreValues, value: Int) { - values.putInteger(key, value) + override fun setValue(values: KeyValueStore, value: Int) { + values.beginWrite().putInteger(key, value).apply() } } -private class FloatValue(private val key: String, private val default: Float) : SignalStoreValueDelegate() { - override fun getValue(values: SignalStoreValues): Float { +private class FloatValue(private val key: String, private val default: Float, store: KeyValueStore) : SignalStoreValueDelegate(store) { + override fun getValue(values: KeyValueStore): Float { return values.getFloat(key, default) } - override fun setValue(values: SignalStoreValues, value: Float) { - values.putFloat(key, value) + override fun setValue(values: KeyValueStore, value: Float) { + values.beginWrite().putFloat(key, value).apply() } } -private class BlobValue(private val key: String, private val default: ByteArray) : SignalStoreValueDelegate() { - override fun getValue(values: SignalStoreValues): ByteArray { +private class BlobValue(private val key: String, private val default: ByteArray, store: KeyValueStore) : SignalStoreValueDelegate(store) { + override fun getValue(values: KeyValueStore): ByteArray { return values.getBlob(key, default) } - override fun setValue(values: SignalStoreValues, value: ByteArray) { - values.putBlob(key, value) + override fun setValue(values: KeyValueStore, value: ByteArray) { + values.beginWrite().putBlob(key, value).apply() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 2c0080459f..d2bab63114 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -97,9 +97,10 @@ public class ApplicationMigrations { static final int FIX_EMOJI_QUALITY = 53; static final int CHANGE_NUMBER_CAPABILITY_4 = 54; static final int KBS_MIGRATION = 55; + static final int PNI_IDENTITY = 56; } - public static final int CURRENT_VERSION = 55; + public static final int CURRENT_VERSION = 56; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -421,6 +422,10 @@ public class ApplicationMigrations { jobs.put(Version.KBS_MIGRATION, new KbsEnclaveMigrationJob()); } + if (lastSeenVersion < Version.PNI_IDENTITY) { + jobs.put(Version.PNI_IDENTITY, new PniAccountInitializationMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index 7eadc5b1da..bef22dc3fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -130,7 +130,7 @@ public class LegacyMigrationJob extends MigrationJob { } if (lastSeenVersion < SIGNED_PREKEY_VERSION) { - ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(context)); + CreateSignedPreKeyJob.enqueueIfNeeded(); } if (lastSeenVersion < NO_DECRYPT_QUEUE_VERSION) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java new file mode 100644 index 0000000000..d0ec9be741 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/PniAccountInitializationMigrationJob.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.jobs.KbsEnclaveMigrationWorkerJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignalProtocolStore; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.PNI; + +import java.io.IOException; +import java.util.List; + +/** + * Initializes various aspects of the PNI identity. Notably: + * - Creates an identity key + * - Creates and uploads one-time prekeys + * - Creates and uploads signed prekeys + */ +public class PniAccountInitializationMigrationJob extends MigrationJob { + + private static final String TAG = Log.tag(PniAccountInitializationMigrationJob.class); + + public static final String KEY = "PniAccountInitializationMigrationJob"; + + PniAccountInitializationMigrationJob() { + this(new Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .build()); + } + + private PniAccountInitializationMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() throws IOException { + PNI pni = SignalStore.account().getPni(); + + if (!Recipient.self().isRegistered() || pni == null) { + Log.w(TAG, "Not yet registered! No need to perform this migration."); + return; + } + + SignalStore.account().generatePniIdentityKey(); + + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + SignalProtocolStore protocolStore = ApplicationDependencies.getProtocolStore().pni(); + PreKeyMetadataStore metadataStore = SignalStore.account().pniPreKeys(); + + SignedPreKeyRecord signedPreKey = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, true); + List oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(protocolStore, metadataStore); + + accountManager.setPreKeys(pni, protocolStore.getIdentityKeyPair().getPublicKey(), signedPreKey, oneTimePreKeys); + metadataStore.setSignedPreKeyRegistered(true); + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return e instanceof IOException; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull PniAccountInitializationMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new PniAccountInitializationMigrationJob(parameters); + } + } +} 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 a1f0e0916b..5311ece533 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -9,11 +9,13 @@ import androidx.annotation.WorkerThread; import org.signal.core.util.logging.Log; import org.signal.zkgroup.profiles.ProfileKey; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.crypto.SessionUtil; +import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore; +import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl; +import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -30,14 +32,15 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAcc import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.AccountIdentifier; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -74,7 +77,7 @@ public final class RegistrationRepository { } public @NonNull ProfileKey getProfileKey(@NonNull String e164) { - ProfileKey profileKey = findExistingProfileKey(context, e164); + ProfileKey profileKey = findExistingProfileKey(e164); if (profileKey == null) { profileKey = ProfileKeyUtil.createNew(); @@ -127,19 +130,22 @@ public final class RegistrationRepository { @Nullable KbsPinData kbsData) throws IOException { - SessionUtil.archiveAllSessions(); - SenderKeyUtil.clearAllState(context); - ACI aci = ACI.parseOrThrow(response.getUuid()); PNI pni = PNI.parseOrThrow(response.getPni()); boolean hasPin = response.isStorageCapable(); - IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); - List records = PreKeyUtil.generatePreKeys(context); - SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true); + SignalStore.account().setAci(aci); + SignalStore.account().setPni(pni); - SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword()); - accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records); + SessionUtil.archiveAllSessions(); + SenderKeyUtil.clearAllState(context); + + SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword()); + SignalServiceAccountDataStoreImpl aciProtocolStore = ApplicationDependencies.getProtocolStore().aci(); + SignalServiceAccountDataStoreImpl pniProtocolStore = ApplicationDependencies.getProtocolStore().pni(); + + generateAndRegisterPreKeys(aci, accountManager, aciProtocolStore, SignalStore.account().aciPreKeys()); + generateAndRegisterPreKeys(pni, accountManager, pniProtocolStore, SignalStore.account().pniPreKeys()); if (registrationData.isFcm()) { accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken())); @@ -151,35 +157,50 @@ public final class RegistrationRepository { recipientDatabase.setProfileSharing(selfId, true); recipientDatabase.markRegisteredOrThrow(selfId, aci); recipientDatabase.setPni(selfId, pni); - - SignalStore.account().setE164(registrationData.getE164()); - SignalStore.account().setAci(aci); - SignalStore.account().setPni(pni); recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey()); + ApplicationDependencies.getRecipientCache().clearSelf(); + SignalStore.account().setE164(registrationData.getE164()); SignalStore.account().setFcmToken(registrationData.getFcmToken()); SignalStore.account().setFcmEnabled(registrationData.isFcm()); - ApplicationDependencies.getProtocolStore().aci().identities() - .saveIdentityWithoutSideEffects(selfId, - identityKey.getPublicKey(), - IdentityDatabase.VerifiedStatus.VERIFIED, - true, - System.currentTimeMillis(), - true); + long now = System.currentTimeMillis(); + saveOwnIdentityKey(selfId, aciProtocolStore, now); + saveOwnIdentityKey(selfId, pniProtocolStore, now); SignalStore.account().setServicePassword(registrationData.getPassword()); SignalStore.account().setRegistered(true); - TextSecurePreferences.setSignedPreKeyRegistered(context, true); TextSecurePreferences.setPromptedPushRegistration(context, true); TextSecurePreferences.setUnauthorizedReceived(context, false); PinState.onRegistration(context, kbsData, pin, hasPin); } + private void generateAndRegisterPreKeys(@NonNull AccountIdentifier accountId, + @NonNull SignalServiceAccountManager accountManager, + @NonNull SignalProtocolStore protocolStore, + @NonNull PreKeyMetadataStore metadataStore) + throws IOException + { + SignedPreKeyRecord signedPreKey = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, true); + List oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(protocolStore, metadataStore); + + accountManager.setPreKeys(accountId, protocolStore.getIdentityKeyPair().getPublicKey(), signedPreKey, oneTimePreKeys); + metadataStore.setSignedPreKeyRegistered(true); + } + + private void saveOwnIdentityKey(@NonNull RecipientId selfId, @NonNull SignalServiceAccountDataStoreImpl protocolStore, long now) { + protocolStore.identities().saveIdentityWithoutSideEffects(selfId, + protocolStore.getIdentityKeyPair().getPublicKey(), + IdentityDatabase.VerifiedStatus.VERIFIED, + true, + now, + true); + } + @WorkerThread - private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) { + private static @Nullable ProfileKey findExistingProfileKey(@NonNull String e164number) { RecipientDatabase recipientDatabase = SignalDatabase.recipients(); Optional recipient = recipientDatabase.getByE164(e164number); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index f59bdd1a81..0b901b3f30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -89,12 +89,10 @@ public class TextSecurePreferences { private static final String SHOW_INVITE_REMINDER_PREF = "pref_show_invite_reminder"; public static final String MESSAGE_BODY_TEXT_SIZE_PREF = "pref_message_body_text_size"; - private static final String SIGNED_PREKEY_REGISTERED_PREF = "pref_signed_prekey_registered"; private static final String WIFI_SMS_PREF = "pref_wifi_sms"; private static final String RATING_LATER_PREF = "pref_rating_later"; private static final String RATING_ENABLED_PREF = "pref_rating_enabled"; - private static final String SIGNED_PREKEY_FAILURE_COUNT_PREF = "pref_signed_prekey_failure_count"; public static final String REPEAT_ALERTS_PREF = "pref_repeat_alerts"; public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy"; @@ -127,10 +125,6 @@ public class TextSecurePreferences { public static final String CALL_RINGTONE_PREF = "pref_call_ringtone"; public static final String CALL_VIBRATE_PREF = "pref_call_vibrate"; - private static final String NEXT_PRE_KEY_ID = "pref_next_pre_key_id"; - private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id"; - private static final String NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id"; - public static final String BACKUP = "pref_backup"; public static final String BACKUP_ENABLED = "pref_backup_enabled"; private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase"; @@ -400,30 +394,6 @@ public class TextSecurePreferences { return getLongPreference(context, BACKUP_TIME, -1); } - public static int getNextPreKeyId(@NonNull Context context) { - return getIntegerPreference(context, NEXT_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE)); - } - - public static void setNextPreKeyId(@NonNull Context context, int value) { - setIntegerPrefrence(context, NEXT_PRE_KEY_ID, value); - } - - public static int getNextSignedPreKeyId(@NonNull Context context) { - return getIntegerPreference(context, NEXT_SIGNED_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE)); - } - - public static void setNextSignedPreKeyId(@NonNull Context context, int value) { - setIntegerPrefrence(context, NEXT_SIGNED_PRE_KEY_ID, value); - } - - public static int getActiveSignedPreKeyId(@NonNull Context context) { - return getIntegerPreference(context, ACTIVE_SIGNED_PRE_KEY_ID, -1); - } - - public static void setActiveSignedPreKeyId(@NonNull Context context, int value) { - setIntegerPrefrence(context, ACTIVE_SIGNED_PRE_KEY_ID, value);; - } - public static void setNeedsSqlCipherMigration(@NonNull Context context, boolean value) { setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value); EventBus.getDefault().post(new SqlCipherMigrationConstraintObserver.SqlCipherNeedsMigrationEvent()); @@ -563,14 +533,6 @@ public class TextSecurePreferences { return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false); } - public static void setSignedPreKeyFailureCount(Context context, int value) { - setIntegerPrefrence(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, value); - } - - public static int getSignedPreKeyFailureCount(Context context) { - return getIntegerPreference(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, 0); - } - @Deprecated public static NotificationPrivacyPreference getNotificationPrivacy(Context context) { return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); @@ -611,14 +573,6 @@ public class TextSecurePreferences { } } - public static boolean isSignedPreKeyRegistered(Context context) { - return getBooleanPreference(context, SIGNED_PREKEY_REGISTERED_PREF, false); - } - - public static void setSignedPreKeyRegistered(Context context, boolean value) { - setBooleanPreference(context, SIGNED_PREKEY_REGISTERED_PREF, value); - } - @Deprecated public static boolean isInThreadNotifications(Context context) { return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true); 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 9f40d400c3..3af6f365a3 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 @@ -416,18 +416,18 @@ public class SignalServiceAccountManager { * * @throws IOException */ - public void setPreKeys(IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List oneTimePreKeys) + public void setPreKeys(AccountIdentifier accountId, IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List oneTimePreKeys) throws IOException { - this.pushServiceSocket.registerPreKeys(identityKey, signedPreKey, oneTimePreKeys); + this.pushServiceSocket.registerPreKeys(accountId, identityKey, signedPreKey, oneTimePreKeys); } /** * @return The server's count of currently available (eg. unused) prekeys for this user. * @throws IOException */ - public int getPreKeysCount() throws IOException { - return this.pushServiceSocket.getAvailablePreKeys(); + public int getPreKeysCount(AccountIdentifier accountId) throws IOException { + return this.pushServiceSocket.getAvailablePreKeys(accountId); } /** @@ -436,16 +436,16 @@ public class SignalServiceAccountManager { * @param signedPreKey The client's new signed prekey. * @throws IOException */ - public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException { - this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey); + public void setSignedPreKey(AccountIdentifier accountId, SignedPreKeyRecord signedPreKey) throws IOException { + this.pushServiceSocket.setCurrentSignedPreKey(accountId, signedPreKey); } /** * @return The server's view of the client's current signed prekey. * @throws IOException */ - public SignedPreKeyEntity getSignedPreKey() throws IOException { - return this.pushServiceSocket.getCurrentSignedPreKey(); + public SignedPreKeyEntity getSignedPreKey(AccountIdentifier accountId) throws IOException { + return this.pushServiceSocket.getCurrentSignedPreKey(accountId); } /** diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java index f6164ba843..ca99bfe130 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java @@ -83,6 +83,11 @@ public final class ACI extends AccountIdentifier { return this.equals(UNKNOWN); } + @Override + public boolean isAci() { + return true; + } + @Override public int hashCode() { return uuid.hashCode(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/AccountIdentifier.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/AccountIdentifier.java index abf8b7378e..3f2b6e9369 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/AccountIdentifier.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/AccountIdentifier.java @@ -17,6 +17,12 @@ public abstract class AccountIdentifier { return uuid; } + public abstract boolean isAci(); + + public final boolean isPni() { + return !isAci(); + } + @Override public String toString() { return uuid.toString(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java index 9ce11caf6e..ef57dd0b41 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java @@ -27,6 +27,11 @@ public final class PNI extends AccountIdentifier { super(uuid); } + @Override + public boolean isAci() { + return false; + } + @Override public int hashCode() { return uuid.hashCode(); 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 47f06190d1..b8cba0bb50 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 @@ -193,10 +193,10 @@ public class PushServiceSocket { private static final String CHANGE_NUMBER_PATH = "/v1/accounts/number"; private static final String IDENTIFIER_REGISTERED_PATH = "/v1/accounts/account/%s"; - private static final String PREKEY_METADATA_PATH = "/v2/keys/"; - private static final String PREKEY_PATH = "/v2/keys/%s"; + 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 SIGNED_PREKEY_PATH = "/v2/keys/signed"; + private static final String SIGNED_PREKEY_PATH = "/v2/keys/signed?identity=%s"; private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code"; private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s"; @@ -563,7 +563,8 @@ public class PushServiceSocket { makeServiceRequest(String.format(UUID_ACK_MESSAGE_PATH, uuid), "DELETE", null); } - public void registerPreKeys(IdentityKey identityKey, + public void registerPreKeys(AccountIdentifier accountId, + IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List records) throws IOException @@ -578,15 +579,17 @@ public class PushServiceSocket { } SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), - signedPreKey.getKeyPair().getPublicKey(), - signedPreKey.getSignature()); + signedPreKey.getKeyPair().getPublicKey(), + signedPreKey.getSignature()); - String response = makeServiceRequest(String.format(PREKEY_PATH, ""), "PUT", - JsonUtil.toJson(new PreKeyState(entities, signedPreKeyEntity, identityKey))); + makeServiceRequest(String.format(Locale.US, PREKEY_PATH, "", accountId.isAci() ? "aci" : "pni"), + "PUT", + JsonUtil.toJson(new PreKeyState(entities, signedPreKeyEntity, identityKey))); } - public int getAvailablePreKeys() throws IOException { - String responseText = makeServiceRequest(PREKEY_METADATA_PATH, "GET", null); + public int getAvailablePreKeys(AccountIdentifier accountId) throws IOException { + String path = String.format(PREKEY_METADATA_PATH, accountId.isAci() ? "aci" : "pni"); + String responseText = makeServiceRequest(path, "GET", null); PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class); return preKeyStatus.getCount(); @@ -675,9 +678,10 @@ public class PushServiceSocket { } } - public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException { + public SignedPreKeyEntity getCurrentSignedPreKey(AccountIdentifier accountId) throws IOException { try { - String responseText = makeServiceRequest(SIGNED_PREKEY_PATH, "GET", null); + String path = String.format(SIGNED_PREKEY_PATH, accountId.isAci() ? "aci" : "pni"); + String responseText = makeServiceRequest(path, "GET", null); return JsonUtil.fromJson(responseText, SignedPreKeyEntity.class); } catch (NotFoundException e) { Log.w(TAG, e); @@ -685,11 +689,12 @@ public class PushServiceSocket { } } - public void setCurrentSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException { + public void setCurrentSignedPreKey(AccountIdentifier accountId, SignedPreKeyRecord signedPreKey) throws IOException { + String path = String.format(SIGNED_PREKEY_PATH, accountId.isAci() ? "aci" : "pni"); SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), signedPreKey.getKeyPair().getPublicKey(), signedPreKey.getSignature()); - makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity)); + makeServiceRequest(path, "PUT", JsonUtil.toJson(signedPreKeyEntity)); } public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)