Support PNI prekeys.

This commit is contained in:
Greyson Parrelli
2022-02-01 14:09:04 -05:00
parent db534cd376
commit e8ad1e8ed1
32 changed files with 808 additions and 532 deletions

View File

@@ -176,7 +176,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeFcmCheck) .addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck) .addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
.addNonBlocking(this::initializePeriodicTasks) .addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention) .addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages) .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() { private void initializeExpiringMessageManager() {
ApplicationDependencies.getExpiringMessageManager().checkSchedule(); ApplicationDependencies.getExpiringMessageManager().checkSchedule();
} }

View File

@@ -63,6 +63,7 @@ public class PassphraseCreateActivity extends PassphraseActivity {
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret); MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
SignalStore.account().generateAciIdentityKey(); SignalStore.account().generateAciIdentityKey();
SignalStore.account().generatePniIdentityKey();
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this); VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
return null; return null;

View File

@@ -17,64 +17,66 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import android.content.Context; import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord; 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.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Medium;
import java.util.Comparator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
public class PreKeyUtil { public class PreKeyUtil {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(PreKeyUtil.class); 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<PreKeyRecord> generateAndStoreOneTimePreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) {
Log.i(TAG, "Generating one-time prekeys...");
public synchronized static List<PreKeyRecord> generatePreKeys(Context context) {
PreKeyStore preKeyStore = ApplicationDependencies.getProtocolStore().aci();
List<PreKeyRecord> records = new LinkedList<>(); List<PreKeyRecord> records = new LinkedList<>();
int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context); int preKeyIdOffset = metadataStore.getNextOneTimePreKeyId();
for (int i=0;i<BATCH_SIZE;i++) { for (int i = 0; i < BATCH_SIZE; i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve.generateKeyPair(); ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
preKeyStore.storePreKey(preKeyId, record); protocolStore.storePreKey(preKeyId, record);
records.add(record); records.add(record);
} }
TextSecurePreferences.setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE); metadataStore.setNextOneTimePreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
return records; return records;
} }
public synchronized static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active) { public synchronized static @NonNull SignedPreKeyRecord generateAndStoreSignedPreKey(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, boolean setAsActive) {
Log.i(TAG, "Generating signed prekeys...");
try { try {
SignedPreKeyStore signedPreKeyStore = ApplicationDependencies.getProtocolStore().aci(); int signedPreKeyId = metadataStore.getNextSignedPreKeyId();
int signedPreKeyId = TextSecurePreferences.getNextSignedPreKeyId(context); ECKeyPair keyPair = Curve.generateKeyPair();
ECKeyPair keyPair = Curve.generateKeyPair(); byte[] signature = Curve.calculateSignature(protocolStore.getIdentityKeyPair().getPrivateKey(), keyPair.getPublicKey().serialize());
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize()); SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record); protocolStore.storeSignedPreKey(signedPreKeyId, record);
TextSecurePreferences.setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE); metadataStore.setNextSignedPreKeyId((signedPreKeyId + 1) % Medium.MAX_VALUE);
if (active) { if (setAsActive) {
TextSecurePreferences.setActiveSignedPreKeyId(context, signedPreKeyId); metadataStore.setActiveSignedPreKeyId(signedPreKeyId);
} }
return record; return record;
@@ -83,12 +85,33 @@ public class PreKeyUtil {
} }
} }
public static synchronized void setActiveSignedPreKeyId(Context context, int id) { /**
TextSecurePreferences.setActiveSignedPreKeyId(context, id); * Finds all of the signed prekeys that are older than the archive age, and archive all but the youngest of those.
} */
public synchronized static void cleanSignedPreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) {
Log.i(TAG, "Cleaning signed prekeys...");
public static synchronized int getActiveSignedPreKeyId(Context context) { int activeSignedPreKeyId = metadataStore.getActiveSignedPreKeyId();
return TextSecurePreferences.getActiveSignedPreKeyId(context); if (activeSignedPreKeyId < 0) {
} return;
}
try {
long now = System.currentTimeMillis();
SignedPreKeyRecord currentRecord = protocolStore.loadSignedPreKey(activeSignedPreKeyId);
List<SignedPreKeyRecord> 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);
}
}
} }

View File

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

View File

@@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.crypto.storage; package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log; 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.PreKeyStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import java.util.List; import java.util.List;
@@ -22,16 +21,16 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
@NonNull @NonNull
private final Context context; private final AccountIdentifier accountId;
public TextSecurePreKeyStore(@NonNull Context context) { public TextSecurePreKeyStore(@NonNull AccountIdentifier accountId) {
this.context = context; this.accountId = accountId;
} }
@Override @Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
synchronized (LOCK) { 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); if (preKeyRecord == null) throw new InvalidKeyIdException("No such key: " + preKeyId);
else return preKeyRecord; else return preKeyRecord;
@@ -41,7 +40,7 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
@Override @Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
synchronized (LOCK) { 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); if (signedPreKeyRecord == null) throw new InvalidKeyIdException("No such signed prekey: " + signedPreKeyId);
else return signedPreKeyRecord; else return signedPreKeyRecord;
@@ -51,41 +50,41 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
@Override @Override
public List<SignedPreKeyRecord> loadSignedPreKeys() { public List<SignedPreKeyRecord> loadSignedPreKeys() {
synchronized (LOCK) { synchronized (LOCK) {
return SignalDatabase.signedPreKeys().getAllSignedPreKeys(); return SignalDatabase.signedPreKeys().getAll(accountId);
} }
} }
@Override @Override
public void storePreKey(int preKeyId, PreKeyRecord record) { public void storePreKey(int preKeyId, PreKeyRecord record) {
synchronized (LOCK) { synchronized (LOCK) {
SignalDatabase.preKeys().insertPreKey(preKeyId, record); SignalDatabase.oneTimePreKeys().insert(accountId, preKeyId, record);
} }
} }
@Override @Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
synchronized (LOCK) { synchronized (LOCK) {
SignalDatabase.signedPreKeys().insertSignedPreKey(signedPreKeyId, record); SignalDatabase.signedPreKeys().insert(accountId, signedPreKeyId, record);
} }
} }
@Override @Override
public boolean containsPreKey(int preKeyId) { public boolean containsPreKey(int preKeyId) {
return SignalDatabase.preKeys().getPreKey(preKeyId) != null; return SignalDatabase.oneTimePreKeys().get(accountId, preKeyId) != null;
} }
@Override @Override
public boolean containsSignedPreKey(int signedPreKeyId) { public boolean containsSignedPreKey(int signedPreKeyId) {
return SignalDatabase.signedPreKeys().getSignedPreKey(signedPreKeyId) != null; return SignalDatabase.signedPreKeys().get(accountId, signedPreKeyId) != null;
} }
@Override @Override
public void removePreKey(int preKeyId) { public void removePreKey(int preKeyId) {
SignalDatabase.preKeys().removePreKey(preKeyId); SignalDatabase.oneTimePreKeys().delete(accountId, preKeyId);
} }
@Override @Override
public void removeSignedPreKey(int signedPreKeyId) { public void removeSignedPreKey(int signedPreKeyId) {
SignalDatabase.signedPreKeys().removeSignedPreKey(signedPreKeyId); SignalDatabase.signedPreKeys().delete(accountId, signedPreKeyId);
} }
} }

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database;
import android.app.Application; import android.app.Application;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.KeyValuePersistentStorage; import org.thoughtcrime.securesms.keyvalue.KeyValuePersistentStorage;
import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.CursorUtil;
import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -60,6 +62,11 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
return instance; return instance;
} }
public static boolean exists(Context context) {
return context.getDatabasePath(DATABASE_NAME).exists();
}
private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) { private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook()); super(application, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, 0,new SqlCipherErrorHandler(DATABASE_NAME), new SqlCipherDatabaseHook());

View File

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

View File

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

View File

@@ -397,8 +397,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
get() = instance!!.pendingRetryReceiptDatabase get() = instance!!.pendingRetryReceiptDatabase
@get:JvmStatic @get:JvmStatic
@get:JvmName("preKeys") @get:JvmName("oneTimePreKeys")
val preKeys: OneTimePreKeyDatabase val oneTimePreKeys: OneTimePreKeyDatabase
get() = instance!!.preKeyDatabase get() = instance!!.preKeyDatabase
@get:Deprecated("This only exists to migrate from legacy storage. There shouldn't be any new usages.") @get:Deprecated("This only exists to migrate from legacy storage. There shouldn't be any new usages.")

View File

@@ -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<SignedPreKeyRecord> getAllSignedPreKeys() {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
List<SignedPreKeyRecord> 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)});
}
}

View File

@@ -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<SignedPreKeyRecord> {
val results: MutableList<SignedPreKeyRecord> = 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))
}
}

View File

@@ -14,6 +14,7 @@ import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -94,7 +95,7 @@ public final class PreKeyMigrationHelper {
reader.close(); reader.close();
Log.i(TAG, "Setting next prekey id: " + index.nextPreKeyId); Log.i(TAG, "Setting next prekey id: " + index.nextPreKeyId);
TextSecurePreferences.setNextPreKeyId(context, index.nextPreKeyId); SignalStore.account().aciPreKeys().setNextOneTimePreKeyId(index.nextPreKeyId);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, 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 next signed prekey id: " + index.nextSignedPreKeyId);
Log.i(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId); Log.i(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId);
TextSecurePreferences.setNextSignedPreKeyId(context, index.nextSignedPreKeyId); SignalStore.account().aciPreKeys().setNextSignedPreKeyId(index.nextSignedPreKeyId);
TextSecurePreferences.setActiveSignedPreKeyId(context, index.activeSignedPreKeyId); SignalStore.account().aciPreKeys().setActiveSignedPreKeyId(index.activeSignedPreKeyId);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.database.helpers package org.thoughtcrime.securesms.database.helpers
import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.content.ContentValues import android.content.ContentValues
import android.content.Context 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.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet 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.RecipientDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
import org.thoughtcrime.securesms.database.requireString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob 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.Stopwatch
import org.thoughtcrime.securesms.util.Triple import org.thoughtcrime.securesms.util.Triple
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.DistributionId import org.whispersystems.signalservice.api.push.DistributionId
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.File import java.io.File
@@ -185,11 +189,12 @@ object SignalDatabaseMigrations {
private const val PNI_CLEANUP = 127 private const val PNI_CLEANUP = 127
private const val MESSAGE_RANGES = 128 private const val MESSAGE_RANGES = 128
private const val REACTION_TRIGGER_FIX = 129 private const val REACTION_TRIGGER_FIX = 129
private const val PNI_STORES = 130
const val DATABASE_VERSION = 129 const val DATABASE_VERSION = 130
@JvmStatic @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) { 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_ringtone TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.id) db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.id)
@@ -2285,6 +2290,80 @@ object SignalDatabaseMigrations {
""".trimIndent() """.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 @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) { private fun migrateReaction(db: SQLiteDatabase, cursor: Cursor, isMms: Boolean) {
try { try {
val messageId = CursorUtil.requireLong(cursor, "_id") val messageId = CursorUtil.requireLong(cursor, "_id")
@@ -2314,4 +2396,22 @@ object SignalDatabaseMigrations {
Log.w(TAG, "Failed to parse reaction!") 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))
}
}
} }

View File

@@ -278,16 +278,27 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
@Override @Override
public @NonNull SignalServiceDataStoreImpl provideProtocolStore() { 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); SignalBaseIdentityKeyStore baseIdentityStore = new SignalBaseIdentityKeyStore(context);
SignalServiceAccountDataStoreImpl aciStore = new SignalServiceAccountDataStoreImpl(context, SignalServiceAccountDataStoreImpl aciStore = new SignalServiceAccountDataStoreImpl(context,
new TextSecurePreKeyStore(context), new TextSecurePreKeyStore(localAci),
new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getAciIdentityKey()), new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getAciIdentityKey()),
new TextSecureSessionStore(context), new TextSecureSessionStore(context),
new SignalSenderKeyStore(context)); new SignalSenderKeyStore(context));
SignalServiceAccountDataStoreImpl pniStore = new SignalServiceAccountDataStoreImpl(context, SignalServiceAccountDataStoreImpl pniStore = new SignalServiceAccountDataStoreImpl(context,
new TextSecurePreKeyStore(context), new TextSecurePreKeyStore(localPni),
new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getPniIdentityKey()), new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getPniIdentityKey()),
new TextSecureSessionStore(context), new TextSecureSessionStore(context),
new SignalSenderKeyStore(context)); new SignalSenderKeyStore(context));

View File

@@ -4,37 +4,24 @@ import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.whispersystems.libsignal.InvalidKeyIdException; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignalProtocolStore;
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;
/**
* Deprecated. Only exists for previously-enqueued jobs.
* Use {@link PreKeyUtil#cleanSignedPreKeys(SignalProtocolStore, PreKeyMetadataStore)} instead.
*/
@Deprecated
public class CleanPreKeysJob extends BaseJob { public class CleanPreKeysJob extends BaseJob {
public static final String KEY = "CleanPreKeysJob"; public static final String KEY = "CleanPreKeysJob";
private static final String TAG = Log.tag(CleanPreKeysJob.class); 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) { private CleanPreKeysJob(@NonNull Job.Parameters parameters) {
super(parameters); super(parameters);
} }
@@ -50,47 +37,13 @@ public class CleanPreKeysJob extends BaseJob {
} }
@Override @Override
public void onRun() throws IOException { public void onRun() {
try { PreKeyUtil.cleanSignedPreKeys(ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys());
Log.i(TAG, "Cleaning prekeys..."); PreKeyUtil.cleanSignedPreKeys(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys());
int activeSignedPreKeyId = PreKeyUtil.getActiveSignedPreKeyId(context);
SignedPreKeyStore signedPreKeyStore = ApplicationDependencies.getProtocolStore().aci();
if (activeSignedPreKeyId < 0) return;
SignedPreKeyRecord currentRecord = signedPreKeyStore.loadSignedPreKey(activeSignedPreKeyId);
List<SignedPreKeyRecord> allRecords = signedPreKeyStore.loadSignedPreKeys();
LinkedList<SignedPreKeyRecord> 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);
}
} }
@Override @Override
public boolean onShouldRetry(@NonNull Exception throwable) { public boolean onShouldRetry(@NonNull Exception throwable) {
if (throwable instanceof NonSuccessfulResponseCodeException) return false;
if (throwable instanceof PushNetworkException) return true;
return false; return false;
} }
@@ -99,30 +52,6 @@ public class CleanPreKeysJob extends BaseJob {
Log.w(TAG, "Failed to execute clean signed prekeys task."); Log.w(TAG, "Failed to execute clean signed prekeys task.");
} }
private LinkedList<SignedPreKeyRecord> removeRecordFrom(SignedPreKeyRecord currentRecord,
List<SignedPreKeyRecord> records)
{
LinkedList<SignedPreKeyRecord> others = new LinkedList<>();
for (SignedPreKeyRecord record : records) {
if (record.getId() != currentRecord.getId()) {
others.add(record);
}
}
return others;
}
private static class SignedPreKeySorter implements Comparator<SignedPreKeyRecord> {
@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<CleanPreKeysJob> { public static final class Factory implements Job.Factory<CleanPreKeysJob> {
@Override @Override
public @NonNull CleanPreKeysJob create(@NonNull Parameters parameters, @NonNull Data data) { public @NonNull CleanPreKeysJob create(@NonNull Parameters parameters, @NonNull Data data) {

View File

@@ -1,36 +1,41 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException; 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 class CreateSignedPreKeyJob extends BaseJob {
public static final String KEY = "CreateSignedPreKeyJob"; public static final String KEY = "CreateSignedPreKeyJob";
private static final String TAG = Log.tag(CreateSignedPreKeyJob.class); private static final String TAG = Log.tag(CreateSignedPreKeyJob.class);
public CreateSignedPreKeyJob(Context context) { private CreateSignedPreKeyJob() {
this(new Job.Parameters.Builder() this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setMaxInstancesForFactory(1)
.setQueue("CreateSignedPreKeyJob") .setQueue("CreateSignedPreKeyJob")
.setMaxAttempts(25) .setLifespan(TimeUnit.DAYS.toMillis(30))
.setMaxAttempts(Parameters.UNLIMITED)
.build()); .build());
} }
@@ -38,6 +43,16 @@ public class CreateSignedPreKeyJob extends BaseJob {
super(parameters); 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 @Override
public @NonNull Data serialize() { public @NonNull Data serialize() {
return Data.EMPTY; return Data.EMPTY;
@@ -50,22 +65,33 @@ public class CreateSignedPreKeyJob extends BaseJob {
@Override @Override
public void onRun() throws IOException { public void onRun() throws IOException {
if (TextSecurePreferences.isSignedPreKeyRegistered(context)) {
Log.w(TAG, "Signed prekey already registered...");
return;
}
if (!SignalStore.account().isRegistered()) { if (!SignalStore.account().isRegistered()) {
Log.w(TAG, "Not yet registered..."); Log.w(TAG, "Not yet registered...");
return; return;
} }
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); createPreKeys(SignalStore.account().getAci(), ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys());
IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey(); createPreKeys(SignalStore.account().getPni(), ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys());
SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKeyPair, true); }
accountManager.setSignedPreKey(signedPreKeyRecord); private void createPreKeys(@Nullable AccountIdentifier accountId, @NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore)
TextSecurePreferences.setSignedPreKeyRegistered(context, true); 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 @Override

View File

@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
import org.thoughtcrime.securesms.migrations.PassingMigrationJob; import org.thoughtcrime.securesms.migrations.PassingMigrationJob;
import org.thoughtcrime.securesms.migrations.PinOptOutMigration; import org.thoughtcrime.securesms.migrations.PinOptOutMigration;
import org.thoughtcrime.securesms.migrations.PinReminderMigrationJob; import org.thoughtcrime.securesms.migrations.PinReminderMigrationJob;
import org.thoughtcrime.securesms.migrations.PniAccountInitializationMigrationJob;
import org.thoughtcrime.securesms.migrations.PniMigrationJob; import org.thoughtcrime.securesms.migrations.PniMigrationJob;
import org.thoughtcrime.securesms.migrations.ProfileMigrationJob; import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob; import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob;
@@ -197,6 +198,7 @@ public final class JobManagerFactories {
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory()); put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory()); put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory());
put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory()); put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory());
put(PniAccountInitializationMigrationJob.KEY, new PniAccountInitializationMigrationJob.Factory());
put(PniMigrationJob.KEY, new PniMigrationJob.Factory()); put(PniMigrationJob.KEY, new PniMigrationJob.Factory());
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory()); put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory()); put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory());

View File

@@ -100,7 +100,7 @@ public abstract class PushSendJob extends SendJob {
@Override @Override
protected final void onSend() throws Exception { protected final void onSend() throws Exception {
if (TextSecurePreferences.getSignedPreKeyFailureCount(context) > 5) { if (SignalStore.account().aciPreKeys().getSignedPreKeyFailureCount() > 5) {
ApplicationDependencies.getJobManager().add(new RotateSignedPreKeyJob()); ApplicationDependencies.getJobManager().add(new RotateSignedPreKeyJob());
throw new TextSecureExpiredException("Too many signed prekey rotation failures"); throw new TextSecureExpiredException("Too many signed prekey rotation failures");
} }

View File

@@ -1,20 +1,24 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; 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.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@@ -22,6 +26,11 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; 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 class RefreshPreKeysJob extends BaseJob {
public static final String KEY = "RefreshPreKeysJob"; public static final String KEY = "RefreshPreKeysJob";
@@ -72,34 +81,61 @@ public class RefreshPreKeysJob extends BaseJob {
return; 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(); if (refreshKeys(aci, aciProtocolStore, aciPreKeyStore)) {
PreKeyUtil.cleanSignedPreKeys(aciProtocolStore, aciPreKeyStore);
Log.i(TAG, "Available keys: " + availableKeys); }
if (availableKeys >= PREKEY_MINIMUM && TextSecurePreferences.isSignedPreKeyRegistered(context)) { if (refreshKeys(pni, pniProtocolStore, pniPreKeyStore)) {
Log.i(TAG, "Available keys sufficient."); PreKeyUtil.cleanSignedPreKeys(pniProtocolStore, pniPreKeyStore);
SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
return;
} }
List<PreKeyRecord> 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()); SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
Log.i(TAG, "Successfully refreshed prekeys."); 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<PreKeyRecord> 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 @Override
public boolean onShouldRetry(@NonNull Exception exception) { public boolean onShouldRetry(@NonNull Exception exception) {
if (exception instanceof NonSuccessfulResponseCodeException) return false; if (exception instanceof NonSuccessfulResponseCodeException) return false;

View File

@@ -4,21 +4,28 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; 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 org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.concurrent.TimeUnit; 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 class RotateSignedPreKeyJob extends BaseJob {
public static final String KEY = "RotateSignedPreKeyJob"; public static final String KEY = "RotateSignedPreKeyJob";
@@ -53,17 +60,34 @@ public class RotateSignedPreKeyJob extends BaseJob {
public void onRun() throws Exception { public void onRun() throws Exception {
Log.i(TAG, "Rotating signed prekey..."); 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(); SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, false);
SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false);
accountManager.setSignedPreKey(signedPreKeyRecord); accountManager.setSignedPreKey(accountId, signedPreKeyRecord);
PreKeyUtil.setActiveSignedPreKeyId(context, signedPreKeyRecord.getId()); metadataStore.setActiveSignedPreKeyId(signedPreKeyRecord.getId());
TextSecurePreferences.setSignedPreKeyRegistered(context, true); metadataStore.setSignedPreKeyRegistered(true);
TextSecurePreferences.setSignedPreKeyFailureCount(context, 0); metadataStore.setSignedPreKeyFailureCount(0);
ApplicationDependencies.getJobManager().add(new CleanPreKeysJob());
} }
@Override @Override
@@ -73,7 +97,11 @@ public class RotateSignedPreKeyJob extends BaseJob {
@Override @Override
public void onFailure() { 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<RotateSignedPreKeyJob> { public static final class Factory implements Job.Factory<RotateSignedPreKeyJob> {

View File

@@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterCipher import org.thoughtcrime.securesms.crypto.MasterCipher
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient 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.IdentityKey
import org.whispersystems.libsignal.IdentityKeyPair import org.whispersystems.libsignal.IdentityKeyPair
import org.whispersystems.libsignal.ecc.Curve 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.ACI
import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.security.SecureRandom
internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) { 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_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_NAME = "account.device_name"
private const val KEY_DEVICE_ID = "account.device_id" 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_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_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_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_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 @VisibleForTesting
const val KEY_E164 = "account.e164" 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. */ /** A randomly-generated value that represents this registration instance. Helps the server know if you reinstalled. */
var registrationId: Int var registrationId: Int by integerValue(KEY_REGISTRATION_ID, 0)
get() = getInteger(KEY_REGISTRATION_ID, 0)
set(value) = putInteger(KEY_REGISTRATION_ID, value)
/** The identity key pair for the ACI identity. */ /** The identity key pair for the ACI identity. */
val aciIdentityKey: IdentityKeyPair val aciIdentityKey: IdentityKeyPair
@@ -147,8 +160,8 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
val key: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() val key: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
store store
.beginWrite() .beginWrite()
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize()) .putBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize())
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize()) .putBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize())
.commit() .commit()
} }
@@ -174,11 +187,27 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, Base64.decode(base64)) 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. */ /** Indicates whether the user has the ability to receive FCM messages. Largely coupled to whether they have Play Service. */
var fcmEnabled: Boolean @get:JvmName("isFcmEnabled")
@JvmName("isFcmEnabled") var fcmEnabled: Boolean by booleanValue(KEY_FCM_ENABLED, false)
get() = getBoolean(KEY_FCM_ENABLED, false)
set(value) = putBoolean(KEY_FCM_ENABLED, value)
/** The FCM token, which allows the server to send us FCM messages. */ /** The FCM token, which allows the server to send us FCM messages. */
var fcmToken: String? var fcmToken: String?
@@ -249,6 +278,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
ApplicationDependencies.getGroupsV2Authorization().clear() ApplicationDependencies.getGroupsV2Authorization().clear()
} }
/** Do not alter. If you need to migrate more stuff, create a new method. */
private fun migrateFromSharedPrefsV1(context: Context) { private fun migrateFromSharedPrefsV1(context: Context) {
Log.i(TAG, "[V1] Migrating account values from shared prefs.") 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)) 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) { private fun migrateFromSharedPrefsV2(context: Context) {
Log.i(TAG, "[V2] Migrating account values from shared prefs.") Log.i(TAG, "[V2] Migrating account values from shared prefs.")
val masterSecretPrefs: SharedPreferences = context.getSharedPreferences("SecureSMS-Preferences", 0) val masterSecretPrefs: SharedPreferences = context.getSharedPreferences("SecureSMS-Preferences", 0)
val defaultPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val defaultPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val storeWriter: KeyValueStore.Writer = store.beginWrite()
if (masterSecretPrefs.hasStringData("pref_identity_public_v3")) { if (masterSecretPrefs.hasStringData("pref_identity_public_v3")) {
Log.i(TAG, "Migrating modern identity key.") Log.i(TAG, "Migrating modern identity key.")
val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_v3", null)!!) val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_v3", null)!!)
val identityPrivate = Base64.decode(masterSecretPrefs.getString("pref_identity_private_v3", null)!!) val identityPrivate = Base64.decode(masterSecretPrefs.getString("pref_identity_private_v3", null)!!)
store storeWriter
.beginWrite()
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic) .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic)
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate) .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate)
.commit()
} else if (masterSecretPrefs.hasStringData("pref_identity_public_curve25519")) { } else if (masterSecretPrefs.hasStringData("pref_identity_public_curve25519")) {
Log.i(TAG, "Migrating legacy identity key.") 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 identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_curve25519", null)!!)
val identityPrivate = masterCipher.decryptKey(Base64.decode(masterSecretPrefs.getString("pref_identity_private_curve25519", null)!!)).serialize() val identityPrivate = masterCipher.decryptKey(Base64.decode(masterSecretPrefs.getString("pref_identity_private_curve25519", null)!!)).serialize()
store storeWriter
.beginWrite()
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic) .putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic)
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate) .putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate)
.commit()
} else { } else {
Log.w(TAG, "No pre-existing identity key! No migration.") 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 masterSecretPrefs
.edit() .edit()
.remove("pref_identity_public_v3") .remove("pref_identity_public_v3")
@@ -308,6 +345,11 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
.edit() .edit()
.remove("pref_local_uuid") .remove("pref_local_uuid")
.remove("pref_identity_public_v3") .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_password")
.remove("pref_gcm_registered") .remove("pref_gcm_registered")
.remove("pref_local_registration_id") .remove("pref_local_registration_id")

View File

@@ -3,102 +3,102 @@ package org.thoughtcrime.securesms.keyvalue
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
internal fun SignalStoreValues.longValue(key: String, default: Long): SignalStoreValueDelegate<Long> { internal fun SignalStoreValues.longValue(key: String, default: Long): SignalStoreValueDelegate<Long> {
return LongValue(key, default) return LongValue(key, default, this.store)
} }
internal fun SignalStoreValues.booleanValue(key: String, default: Boolean): SignalStoreValueDelegate<Boolean> { internal fun SignalStoreValues.booleanValue(key: String, default: Boolean): SignalStoreValueDelegate<Boolean> {
return BooleanValue(key, default) return BooleanValue(key, default, this.store)
} }
internal fun <T : String?> SignalStoreValues.stringValue(key: String, default: T): SignalStoreValueDelegate<T> { internal fun <T : String?> SignalStoreValues.stringValue(key: String, default: T): SignalStoreValueDelegate<T> {
return StringValue(key, default) return StringValue(key, default, this.store)
} }
internal fun SignalStoreValues.integerValue(key: String, default: Int): SignalStoreValueDelegate<Int> { internal fun SignalStoreValues.integerValue(key: String, default: Int): SignalStoreValueDelegate<Int> {
return IntValue(key, default) return IntValue(key, default, this.store)
} }
internal fun SignalStoreValues.floatValue(key: String, default: Float): SignalStoreValueDelegate<Float> { internal fun SignalStoreValues.floatValue(key: String, default: Float): SignalStoreValueDelegate<Float> {
return FloatValue(key, default) return FloatValue(key, default, this.store)
} }
internal fun SignalStoreValues.blobValue(key: String, default: ByteArray): SignalStoreValueDelegate<ByteArray> { internal fun SignalStoreValues.blobValue(key: String, default: ByteArray): SignalStoreValueDelegate<ByteArray> {
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 * 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. * class to callers and protect the individual implementations as private behind the various extension functions.
*/ */
sealed class SignalStoreValueDelegate<T> { sealed class SignalStoreValueDelegate<T>(private val store: KeyValueStore) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T { 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) { 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 getValue(values: KeyValueStore): T
internal abstract fun setValue(values: SignalStoreValues, value: T) internal abstract fun setValue(values: KeyValueStore, value: T)
} }
private class LongValue(private val key: String, private val default: Long) : SignalStoreValueDelegate<Long>() { private class LongValue(private val key: String, private val default: Long, store: KeyValueStore) : SignalStoreValueDelegate<Long>(store) {
override fun getValue(values: SignalStoreValues): Long { override fun getValue(values: KeyValueStore): Long {
return values.getLong(key, default) return values.getLong(key, default)
} }
override fun setValue(values: SignalStoreValues, value: Long) { override fun setValue(values: KeyValueStore, value: Long) {
values.putLong(key, value) values.beginWrite().putLong(key, value).apply()
} }
} }
private class BooleanValue(private val key: String, private val default: Boolean) : SignalStoreValueDelegate<Boolean>() { private class BooleanValue(private val key: String, private val default: Boolean, store: KeyValueStore) : SignalStoreValueDelegate<Boolean>(store) {
override fun getValue(values: SignalStoreValues): Boolean { override fun getValue(values: KeyValueStore): Boolean {
return values.getBoolean(key, default) return values.getBoolean(key, default)
} }
override fun setValue(values: SignalStoreValues, value: Boolean) { override fun setValue(values: KeyValueStore, value: Boolean) {
values.putBoolean(key, value) values.beginWrite().putBoolean(key, value).apply()
} }
} }
private class StringValue<T : String?>(private val key: String, private val default: T) : SignalStoreValueDelegate<T>() { private class StringValue<T : String?>(private val key: String, private val default: T, store: KeyValueStore) : SignalStoreValueDelegate<T>(store) {
override fun getValue(values: SignalStoreValues): T { override fun getValue(values: KeyValueStore): T {
return values.getString(key, default) as T return values.getString(key, default) as T
} }
override fun setValue(values: SignalStoreValues, value: T) { override fun setValue(values: KeyValueStore, value: T) {
values.putString(key, value) values.beginWrite().putString(key, value).apply()
} }
} }
private class IntValue(private val key: String, private val default: Int) : SignalStoreValueDelegate<Int>() { private class IntValue(private val key: String, private val default: Int, store: KeyValueStore) : SignalStoreValueDelegate<Int>(store) {
override fun getValue(values: SignalStoreValues): Int { override fun getValue(values: KeyValueStore): Int {
return values.getInteger(key, default) return values.getInteger(key, default)
} }
override fun setValue(values: SignalStoreValues, value: Int) { override fun setValue(values: KeyValueStore, value: Int) {
values.putInteger(key, value) values.beginWrite().putInteger(key, value).apply()
} }
} }
private class FloatValue(private val key: String, private val default: Float) : SignalStoreValueDelegate<Float>() { private class FloatValue(private val key: String, private val default: Float, store: KeyValueStore) : SignalStoreValueDelegate<Float>(store) {
override fun getValue(values: SignalStoreValues): Float { override fun getValue(values: KeyValueStore): Float {
return values.getFloat(key, default) return values.getFloat(key, default)
} }
override fun setValue(values: SignalStoreValues, value: Float) { override fun setValue(values: KeyValueStore, value: Float) {
values.putFloat(key, value) values.beginWrite().putFloat(key, value).apply()
} }
} }
private class BlobValue(private val key: String, private val default: ByteArray) : SignalStoreValueDelegate<ByteArray>() { private class BlobValue(private val key: String, private val default: ByteArray, store: KeyValueStore) : SignalStoreValueDelegate<ByteArray>(store) {
override fun getValue(values: SignalStoreValues): ByteArray { override fun getValue(values: KeyValueStore): ByteArray {
return values.getBlob(key, default) return values.getBlob(key, default)
} }
override fun setValue(values: SignalStoreValues, value: ByteArray) { override fun setValue(values: KeyValueStore, value: ByteArray) {
values.putBlob(key, value) values.beginWrite().putBlob(key, value).apply()
} }
} }

View File

@@ -97,9 +97,10 @@ public class ApplicationMigrations {
static final int FIX_EMOJI_QUALITY = 53; static final int FIX_EMOJI_QUALITY = 53;
static final int CHANGE_NUMBER_CAPABILITY_4 = 54; static final int CHANGE_NUMBER_CAPABILITY_4 = 54;
static final int KBS_MIGRATION = 55; 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 * 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()); jobs.put(Version.KBS_MIGRATION, new KbsEnclaveMigrationJob());
} }
if (lastSeenVersion < Version.PNI_IDENTITY) {
jobs.put(Version.PNI_IDENTITY, new PniAccountInitializationMigrationJob());
}
return jobs; return jobs;
} }

View File

@@ -130,7 +130,7 @@ public class LegacyMigrationJob extends MigrationJob {
} }
if (lastSeenVersion < SIGNED_PREKEY_VERSION) { if (lastSeenVersion < SIGNED_PREKEY_VERSION) {
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(context)); CreateSignedPreKeyJob.enqueueIfNeeded();
} }
if (lastSeenVersion < NO_DECRYPT_QUEUE_VERSION) { if (lastSeenVersion < NO_DECRYPT_QUEUE_VERSION) {

View File

@@ -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<PreKeyRecord> 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<PniAccountInitializationMigrationJob> {
@Override
public @NonNull PniAccountInitializationMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new PniAccountInitializationMigrationJob(parameters);
}
}
}

View File

@@ -9,11 +9,13 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil; 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.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase; 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.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ACI; 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.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.ServiceResponse;
@@ -74,7 +77,7 @@ public final class RegistrationRepository {
} }
public @NonNull ProfileKey getProfileKey(@NonNull String e164) { public @NonNull ProfileKey getProfileKey(@NonNull String e164) {
ProfileKey profileKey = findExistingProfileKey(context, e164); ProfileKey profileKey = findExistingProfileKey(e164);
if (profileKey == null) { if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew(); profileKey = ProfileKeyUtil.createNew();
@@ -127,19 +130,22 @@ public final class RegistrationRepository {
@Nullable KbsPinData kbsData) @Nullable KbsPinData kbsData)
throws IOException throws IOException
{ {
SessionUtil.archiveAllSessions();
SenderKeyUtil.clearAllState(context);
ACI aci = ACI.parseOrThrow(response.getUuid()); ACI aci = ACI.parseOrThrow(response.getUuid());
PNI pni = PNI.parseOrThrow(response.getPni()); PNI pni = PNI.parseOrThrow(response.getPni());
boolean hasPin = response.isStorageCapable(); boolean hasPin = response.isStorageCapable();
IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey(); SignalStore.account().setAci(aci);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context); SignalStore.account().setPni(pni);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword()); SessionUtil.archiveAllSessions();
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records); 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()) { if (registrationData.isFcm()) {
accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken())); accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken()));
@@ -151,35 +157,50 @@ public final class RegistrationRepository {
recipientDatabase.setProfileSharing(selfId, true); recipientDatabase.setProfileSharing(selfId, true);
recipientDatabase.markRegisteredOrThrow(selfId, aci); recipientDatabase.markRegisteredOrThrow(selfId, aci);
recipientDatabase.setPni(selfId, pni); recipientDatabase.setPni(selfId, pni);
SignalStore.account().setE164(registrationData.getE164());
SignalStore.account().setAci(aci);
SignalStore.account().setPni(pni);
recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey()); recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey());
ApplicationDependencies.getRecipientCache().clearSelf(); ApplicationDependencies.getRecipientCache().clearSelf();
SignalStore.account().setE164(registrationData.getE164());
SignalStore.account().setFcmToken(registrationData.getFcmToken()); SignalStore.account().setFcmToken(registrationData.getFcmToken());
SignalStore.account().setFcmEnabled(registrationData.isFcm()); SignalStore.account().setFcmEnabled(registrationData.isFcm());
ApplicationDependencies.getProtocolStore().aci().identities() long now = System.currentTimeMillis();
.saveIdentityWithoutSideEffects(selfId, saveOwnIdentityKey(selfId, aciProtocolStore, now);
identityKey.getPublicKey(), saveOwnIdentityKey(selfId, pniProtocolStore, now);
IdentityDatabase.VerifiedStatus.VERIFIED,
true,
System.currentTimeMillis(),
true);
SignalStore.account().setServicePassword(registrationData.getPassword()); SignalStore.account().setServicePassword(registrationData.getPassword());
SignalStore.account().setRegistered(true); SignalStore.account().setRegistered(true);
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true); TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false); TextSecurePreferences.setUnauthorizedReceived(context, false);
PinState.onRegistration(context, kbsData, pin, hasPin); 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<PreKeyRecord> 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 @WorkerThread
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) { private static @Nullable ProfileKey findExistingProfileKey(@NonNull String e164number) {
RecipientDatabase recipientDatabase = SignalDatabase.recipients(); RecipientDatabase recipientDatabase = SignalDatabase.recipients();
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number); Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);

View File

@@ -89,12 +89,10 @@ public class TextSecurePreferences {
private static final String SHOW_INVITE_REMINDER_PREF = "pref_show_invite_reminder"; 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"; 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 WIFI_SMS_PREF = "pref_wifi_sms";
private static final String RATING_LATER_PREF = "pref_rating_later"; private static final String RATING_LATER_PREF = "pref_rating_later";
private static final String RATING_ENABLED_PREF = "pref_rating_enabled"; 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 REPEAT_ALERTS_PREF = "pref_repeat_alerts";
public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy"; 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_RINGTONE_PREF = "pref_call_ringtone";
public static final String CALL_VIBRATE_PREF = "pref_call_vibrate"; 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 = "pref_backup";
public static final String BACKUP_ENABLED = "pref_backup_enabled"; public static final String BACKUP_ENABLED = "pref_backup_enabled";
private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase"; private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase";
@@ -400,30 +394,6 @@ public class TextSecurePreferences {
return getLongPreference(context, BACKUP_TIME, -1); 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) { public static void setNeedsSqlCipherMigration(@NonNull Context context, boolean value) {
setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value); setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value);
EventBus.getDefault().post(new SqlCipherMigrationConstraintObserver.SqlCipherNeedsMigrationEvent()); EventBus.getDefault().post(new SqlCipherMigrationConstraintObserver.SqlCipherNeedsMigrationEvent());
@@ -563,14 +533,6 @@ public class TextSecurePreferences {
return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false); 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 @Deprecated
public static NotificationPrivacyPreference getNotificationPrivacy(Context context) { public static NotificationPrivacyPreference getNotificationPrivacy(Context context) {
return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")); 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 @Deprecated
public static boolean isInThreadNotifications(Context context) { public static boolean isInThreadNotifications(Context context) {
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true); return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);

View File

@@ -416,18 +416,18 @@ public class SignalServiceAccountManager {
* *
* @throws IOException * @throws IOException
*/ */
public void setPreKeys(IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys) public void setPreKeys(AccountIdentifier accountId, IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)
throws IOException 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. * @return The server's count of currently available (eg. unused) prekeys for this user.
* @throws IOException * @throws IOException
*/ */
public int getPreKeysCount() throws IOException { public int getPreKeysCount(AccountIdentifier accountId) throws IOException {
return this.pushServiceSocket.getAvailablePreKeys(); return this.pushServiceSocket.getAvailablePreKeys(accountId);
} }
/** /**
@@ -436,16 +436,16 @@ public class SignalServiceAccountManager {
* @param signedPreKey The client's new signed prekey. * @param signedPreKey The client's new signed prekey.
* @throws IOException * @throws IOException
*/ */
public void setSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException { public void setSignedPreKey(AccountIdentifier accountId, SignedPreKeyRecord signedPreKey) throws IOException {
this.pushServiceSocket.setCurrentSignedPreKey(signedPreKey); this.pushServiceSocket.setCurrentSignedPreKey(accountId, signedPreKey);
} }
/** /**
* @return The server's view of the client's current signed prekey. * @return The server's view of the client's current signed prekey.
* @throws IOException * @throws IOException
*/ */
public SignedPreKeyEntity getSignedPreKey() throws IOException { public SignedPreKeyEntity getSignedPreKey(AccountIdentifier accountId) throws IOException {
return this.pushServiceSocket.getCurrentSignedPreKey(); return this.pushServiceSocket.getCurrentSignedPreKey(accountId);
} }
/** /**

View File

@@ -83,6 +83,11 @@ public final class ACI extends AccountIdentifier {
return this.equals(UNKNOWN); return this.equals(UNKNOWN);
} }
@Override
public boolean isAci() {
return true;
}
@Override @Override
public int hashCode() { public int hashCode() {
return uuid.hashCode(); return uuid.hashCode();

View File

@@ -17,6 +17,12 @@ public abstract class AccountIdentifier {
return uuid; return uuid;
} }
public abstract boolean isAci();
public final boolean isPni() {
return !isAci();
}
@Override @Override
public String toString() { public String toString() {
return uuid.toString(); return uuid.toString();

View File

@@ -27,6 +27,11 @@ public final class PNI extends AccountIdentifier {
super(uuid); super(uuid);
} }
@Override
public boolean isAci() {
return false;
}
@Override @Override
public int hashCode() { public int hashCode() {
return uuid.hashCode(); return uuid.hashCode();

View File

@@ -193,10 +193,10 @@ public class PushServiceSocket {
private static final String CHANGE_NUMBER_PATH = "/v1/accounts/number"; 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 IDENTIFIER_REGISTERED_PATH = "/v1/accounts/account/%s";
private static final String PREKEY_METADATA_PATH = "/v2/keys/"; private static final String PREKEY_METADATA_PATH = "/v2/keys?identity=%s";
private static final String PREKEY_PATH = "/v2/keys/%s"; private static final String PREKEY_PATH = "/v2/keys/%s?identity=%s";
private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s"; private static final String PREKEY_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_CODE_PATH = "/v1/devices/provisioning/code";
private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s"; 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); makeServiceRequest(String.format(UUID_ACK_MESSAGE_PATH, uuid), "DELETE", null);
} }
public void registerPreKeys(IdentityKey identityKey, public void registerPreKeys(AccountIdentifier accountId,
IdentityKey identityKey,
SignedPreKeyRecord signedPreKey, SignedPreKeyRecord signedPreKey,
List<PreKeyRecord> records) List<PreKeyRecord> records)
throws IOException throws IOException
@@ -578,15 +579,17 @@ public class PushServiceSocket {
} }
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(), signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature()); signedPreKey.getSignature());
String response = makeServiceRequest(String.format(PREKEY_PATH, ""), "PUT", makeServiceRequest(String.format(Locale.US, PREKEY_PATH, "", accountId.isAci() ? "aci" : "pni"),
JsonUtil.toJson(new PreKeyState(entities, signedPreKeyEntity, identityKey))); "PUT",
JsonUtil.toJson(new PreKeyState(entities, signedPreKeyEntity, identityKey)));
} }
public int getAvailablePreKeys() throws IOException { public int getAvailablePreKeys(AccountIdentifier accountId) throws IOException {
String responseText = makeServiceRequest(PREKEY_METADATA_PATH, "GET", null); String path = String.format(PREKEY_METADATA_PATH, accountId.isAci() ? "aci" : "pni");
String responseText = makeServiceRequest(path, "GET", null);
PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class); PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class);
return preKeyStatus.getCount(); return preKeyStatus.getCount();
@@ -675,9 +678,10 @@ public class PushServiceSocket {
} }
} }
public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException { public SignedPreKeyEntity getCurrentSignedPreKey(AccountIdentifier accountId) throws IOException {
try { 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); return JsonUtil.fromJson(responseText, SignedPreKeyEntity.class);
} catch (NotFoundException e) { } catch (NotFoundException e) {
Log.w(TAG, 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(), SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(), signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature()); 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) public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)