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::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
.addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages)
@ -352,12 +352,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
}
private void initializeSignedPreKeyCheck() {
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this));
}
}
private void initializeExpiringMessageManager() {
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
}

View file

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

View file

@ -17,64 +17,66 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.libsignal.util.Medium;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class PreKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(PreKeyUtil.class);
private static final int BATCH_SIZE = 100;
private static final int BATCH_SIZE = 100;
private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30);
public synchronized static @NonNull List<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<>();
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;
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
preKeyStore.storePreKey(preKeyId, record);
protocolStore.storePreKey(preKeyId, 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;
}
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 {
SignedPreKeyStore signedPreKeyStore = ApplicationDependencies.getProtocolStore().aci();
int signedPreKeyId = TextSecurePreferences.getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
int signedPreKeyId = metadataStore.getNextSignedPreKeyId();
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(protocolStore.getIdentityKeyPair().getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
TextSecurePreferences.setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
protocolStore.storeSignedPreKey(signedPreKeyId, record);
metadataStore.setNextSignedPreKeyId((signedPreKeyId + 1) % Medium.MAX_VALUE);
if (active) {
TextSecurePreferences.setActiveSignedPreKeyId(context, signedPreKeyId);
if (setAsActive) {
metadataStore.setActiveSignedPreKeyId(signedPreKeyId);
}
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) {
return TextSecurePreferences.getActiveSignedPreKeyId(context);
}
int activeSignedPreKeyId = metadataStore.getActiveSignedPreKeyId();
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;
import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
@ -11,6 +9,7 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import java.util.List;
@ -22,16 +21,16 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
private static final Object LOCK = new Object();
@NonNull
private final Context context;
private final AccountIdentifier accountId;
public TextSecurePreKeyStore(@NonNull Context context) {
this.context = context;
public TextSecurePreKeyStore(@NonNull AccountIdentifier accountId) {
this.accountId = accountId;
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
synchronized (LOCK) {
PreKeyRecord preKeyRecord = SignalDatabase.preKeys().getPreKey(preKeyId);
PreKeyRecord preKeyRecord = SignalDatabase.oneTimePreKeys().get(accountId, preKeyId);
if (preKeyRecord == null) throw new InvalidKeyIdException("No such key: " + preKeyId);
else return preKeyRecord;
@ -41,7 +40,7 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
synchronized (LOCK) {
SignedPreKeyRecord signedPreKeyRecord = SignalDatabase.signedPreKeys().getSignedPreKey(signedPreKeyId);
SignedPreKeyRecord signedPreKeyRecord = SignalDatabase.signedPreKeys().get(accountId, signedPreKeyId);
if (signedPreKeyRecord == null) throw new InvalidKeyIdException("No such signed prekey: " + signedPreKeyId);
else return signedPreKeyRecord;
@ -51,41 +50,41 @@ public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
synchronized (LOCK) {
return SignalDatabase.signedPreKeys().getAllSignedPreKeys();
return SignalDatabase.signedPreKeys().getAll(accountId);
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
synchronized (LOCK) {
SignalDatabase.preKeys().insertPreKey(preKeyId, record);
SignalDatabase.oneTimePreKeys().insert(accountId, preKeyId, record);
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
synchronized (LOCK) {
SignalDatabase.signedPreKeys().insertSignedPreKey(signedPreKeyId, record);
SignalDatabase.signedPreKeys().insert(accountId, signedPreKeyId, record);
}
}
@Override
public boolean containsPreKey(int preKeyId) {
return SignalDatabase.preKeys().getPreKey(preKeyId) != null;
return SignalDatabase.oneTimePreKeys().get(accountId, preKeyId) != null;
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return SignalDatabase.signedPreKeys().getSignedPreKey(signedPreKeyId) != null;
return SignalDatabase.signedPreKeys().get(accountId, signedPreKeyId) != null;
}
@Override
public void removePreKey(int preKeyId) {
SignalDatabase.preKeys().removePreKey(preKeyId);
SignalDatabase.oneTimePreKeys().delete(accountId, preKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
SignalDatabase.signedPreKeys().removeSignedPreKey(signedPreKeyId);
SignalDatabase.signedPreKeys().delete(accountId, signedPreKeyId);
}
}

View file

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

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:JvmStatic
@get:JvmName("preKeys")
val preKeys: OneTimePreKeyDatabase
@get:JvmName("oneTimePreKeys")
val oneTimePreKeys: OneTimePreKeyDatabase
get() = instance!!.preKeyDatabase
@get:Deprecated("This only exists to migrate from legacy storage. There shouldn't be any new usages.")

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

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.database.helpers
import android.app.Application
import android.app.NotificationChannel
import android.content.ContentValues
import android.content.Context
@ -18,8 +19,10 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
import org.thoughtcrime.securesms.database.requireString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob
@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.util.SqlUtil
import org.thoughtcrime.securesms.util.Stopwatch
import org.thoughtcrime.securesms.util.Triple
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.DistributionId
import java.io.ByteArrayInputStream
import java.io.File
@ -185,11 +189,12 @@ object SignalDatabaseMigrations {
private const val PNI_CLEANUP = 127
private const val MESSAGE_RANGES = 128
private const val REACTION_TRIGGER_FIX = 129
private const val PNI_STORES = 130
const val DATABASE_VERSION = 129
const val DATABASE_VERSION = 130
@JvmStatic
fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) {
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.id)
@ -2285,6 +2290,80 @@ object SignalDatabaseMigrations {
""".trimIndent()
)
}
if (oldVersion < PNI_STORES) {
val localAci: ACI? = getLocalAci(context)
// One-Time Prekeys
db.execSQL(
"""
CREATE TABLE one_time_prekeys_tmp (
_id INTEGER PRIMARY KEY,
account_id TEXT NOT NULL,
key_id INTEGER,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
UNIQUE(account_id, key_id)
)
""".trimIndent()
)
if (localAci != null) {
db.execSQL(
"""
INSERT INTO one_time_prekeys_tmp (account_id, key_id, public_key, private_key)
SELECT
'$localAci' AS account_id,
one_time_prekeys.key_id,
one_time_prekeys.public_key,
one_time_prekeys.private_key
FROM one_time_prekeys
""".trimIndent()
)
} else {
Log.w(TAG, "No local ACI set. Not migrating any existing one-time prekeys.")
}
db.execSQL("DROP TABLE one_time_prekeys")
db.execSQL("ALTER TABLE one_time_prekeys_tmp RENAME TO one_time_prekeys")
// Signed Prekeys
db.execSQL(
"""
CREATE TABLE signed_prekeys_tmp (
_id INTEGER PRIMARY KEY,
account_id TEXT NOT NULL,
key_id INTEGER,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL,
signature TEXT NOT NULL,
timestamp INTEGER DEFAULT 0,
UNIQUE(account_id, key_id)
)
""".trimIndent()
)
if (localAci != null) {
db.execSQL(
"""
INSERT INTO signed_prekeys_tmp (account_id, key_id, public_key, private_key, signature, timestamp)
SELECT
'$localAci' AS account_id,
signed_prekeys.key_id,
signed_prekeys.public_key,
signed_prekeys.private_key,
signed_prekeys.signature,
signed_prekeys.timestamp
FROM signed_prekeys
""".trimIndent()
)
} else {
Log.w(TAG, "No local ACI set. Not migrating any existing signed prekeys.")
}
db.execSQL("DROP TABLE signed_prekeys")
db.execSQL("ALTER TABLE signed_prekeys_tmp RENAME TO signed_prekeys")
}
}
@JvmStatic
@ -2294,6 +2373,9 @@ object SignalDatabaseMigrations {
}
}
/**
* Important: You can't change this method, or you risk breaking existing migrations. If you need to change this, make a new method.
*/
private fun migrateReaction(db: SQLiteDatabase, cursor: Cursor, isMms: Boolean) {
try {
val messageId = CursorUtil.requireLong(cursor, "_id")
@ -2314,4 +2396,22 @@ object SignalDatabaseMigrations {
Log.w(TAG, "Failed to parse reaction!")
}
}
/**
* Important: You can't change this method, or you risk breaking existing migrations. If you need to change this, make a new method.
*/
private fun getLocalAci(context: Application): ACI? {
if (KeyValueDatabase.exists(context)) {
val keyValueDatabase = KeyValueDatabase.getInstance(context).readableDatabase
keyValueDatabase.query("key_value", arrayOf("value"), "key = ?", SqlUtil.buildArgs("account.aci"), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
ACI.parseOrNull(cursor.requireString("value"))
} else {
null
}
}
} else {
return ACI.parseOrNull(PreferenceManager.getDefaultSharedPreferences(context).getString("pref_local_uuid", null))
}
}
}

View file

@ -278,16 +278,27 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
@Override
public @NonNull SignalServiceDataStoreImpl provideProtocolStore() {
ACI localAci = SignalStore.account().getAci();
PNI localPni = SignalStore.account().getPni();
if (localAci == null) {
throw new IllegalStateException("No ACI set!");
}
if (localPni == null) {
throw new IllegalStateException("No PNI set!");
}
SignalBaseIdentityKeyStore baseIdentityStore = new SignalBaseIdentityKeyStore(context);
SignalServiceAccountDataStoreImpl aciStore = new SignalServiceAccountDataStoreImpl(context,
new TextSecurePreKeyStore(context),
new TextSecurePreKeyStore(localAci),
new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getAciIdentityKey()),
new TextSecureSessionStore(context),
new SignalSenderKeyStore(context));
SignalServiceAccountDataStoreImpl pniStore = new SignalServiceAccountDataStoreImpl(context,
new TextSecurePreKeyStore(context),
new TextSecurePreKeyStore(localPni),
new SignalIdentityKeyStore(baseIdentityStore, () -> SignalStore.account().getPniIdentityKey()),
new TextSecureSessionStore(context),
new SignalSenderKeyStore(context));

View file

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

View file

@ -1,36 +1,41 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* Creates and uploads a new signed prekey for an identity if one hasn't been uploaded yet.
*/
public class CreateSignedPreKeyJob extends BaseJob {
public static final String KEY = "CreateSignedPreKeyJob";
private static final String TAG = Log.tag(CreateSignedPreKeyJob.class);
public CreateSignedPreKeyJob(Context context) {
private CreateSignedPreKeyJob() {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxInstancesForFactory(1)
.setQueue("CreateSignedPreKeyJob")
.setMaxAttempts(25)
.setLifespan(TimeUnit.DAYS.toMillis(30))
.setMaxAttempts(Parameters.UNLIMITED)
.build());
}
@ -38,6 +43,16 @@ public class CreateSignedPreKeyJob extends BaseJob {
super(parameters);
}
/**
* Enqueues an instance of this job if we not yet created + uploaded signed prekeys for one of our identities.
*/
public static void enqueueIfNeeded() {
if (!SignalStore.account().aciPreKeys().isSignedPreKeyRegistered() || !SignalStore.account().pniPreKeys().isSignedPreKeyRegistered()) {
Log.i(TAG, "Some signed prekeys aren't registered yet. Enqueuing a job.");
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob());
}
}
@Override
public @NonNull Data serialize() {
return Data.EMPTY;
@ -50,22 +65,33 @@ public class CreateSignedPreKeyJob extends BaseJob {
@Override
public void onRun() throws IOException {
if (TextSecurePreferences.isSignedPreKeyRegistered(context)) {
Log.w(TAG, "Signed prekey already registered...");
return;
}
if (!SignalStore.account().isRegistered()) {
Log.w(TAG, "Not yet registered...");
return;
}
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey();
SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKeyPair, true);
createPreKeys(SignalStore.account().getAci(), ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys());
createPreKeys(SignalStore.account().getPni(), ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys());
}
accountManager.setSignedPreKey(signedPreKeyRecord);
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
private void createPreKeys(@Nullable AccountIdentifier accountId, @NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore)
throws IOException
{
if (accountId == null) {
warn(TAG, "AccountId not set!");
return;
}
if (metadataStore.isSignedPreKeyRegistered()) {
warn(TAG, "Signed prekey for " + (accountId.isAci() ? "ACI" : "PNI") + " already registered...");
return;
}
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, true);
accountManager.setSignedPreKey(accountId, signedPreKeyRecord);
metadataStore.setSignedPreKeyRegistered(true);
}
@Override

View file

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

View file

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

View file

@ -1,20 +1,24 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@ -22,6 +26,11 @@ import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Ensures that our prekeys are up to date for both our ACI and PNI identities.
* Specifically, if we have less than {@link #PREKEY_MINIMUM} one-time prekeys, we will generate and upload
* a new batch of one-time prekeys, as well as a new signed prekey.
*/
public class RefreshPreKeysJob extends BaseJob {
public static final String KEY = "RefreshPreKeysJob";
@ -72,34 +81,61 @@ public class RefreshPreKeysJob extends BaseJob {
return;
}
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
ACI aci = SignalStore.account().getAci();
SignalProtocolStore aciProtocolStore = ApplicationDependencies.getProtocolStore().aci();
PreKeyMetadataStore aciPreKeyStore = SignalStore.account().aciPreKeys();
PNI pni = SignalStore.account().getPni();
SignalProtocolStore pniProtocolStore = ApplicationDependencies.getProtocolStore().pni();
PreKeyMetadataStore pniPreKeyStore = SignalStore.account().pniPreKeys();
int availableKeys = accountManager.getPreKeysCount();
Log.i(TAG, "Available keys: " + availableKeys);
if (availableKeys >= PREKEY_MINIMUM && TextSecurePreferences.isSignedPreKeyRegistered(context)) {
Log.i(TAG, "Available keys sufficient.");
SignalStore.misc().setLastPrekeyRefreshTime(System.currentTimeMillis());
return;
if (refreshKeys(aci, aciProtocolStore, aciPreKeyStore)) {
PreKeyUtil.cleanSignedPreKeys(aciProtocolStore, aciPreKeyStore);
}
if (refreshKeys(pni, pniProtocolStore, pniPreKeyStore)) {
PreKeyUtil.cleanSignedPreKeys(pniProtocolStore, pniPreKeyStore);
}
List<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());
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
public boolean onShouldRetry(@NonNull Exception exception) {
if (exception instanceof NonSuccessfulResponseCodeException) return false;

View file

@ -4,21 +4,28 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* Forces the creation + upload of new signed prekeys for both the ACI and PNI identities.
*/
public class RotateSignedPreKeyJob extends BaseJob {
public static final String KEY = "RotateSignedPreKeyJob";
@ -53,17 +60,34 @@ public class RotateSignedPreKeyJob extends BaseJob {
public void onRun() throws Exception {
Log.i(TAG, "Rotating signed prekey...");
ACI aci = SignalStore.account().getAci();
PNI pni = SignalStore.account().getPni();
if (aci == null) {
Log.w(TAG, "ACI is unset!");
return;
}
if (pni == null) {
Log.w(TAG, "PNI is unset!");
return;
}
rotate(aci, ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys());
rotate(pni, ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys());
}
private void rotate(@NonNull AccountIdentifier accountId, @NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore)
throws IOException
{
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey();
SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(context, identityKey, false);
SignedPreKeyRecord signedPreKeyRecord = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, false);
accountManager.setSignedPreKey(signedPreKeyRecord);
accountManager.setSignedPreKey(accountId, signedPreKeyRecord);
PreKeyUtil.setActiveSignedPreKeyId(context, signedPreKeyRecord.getId());
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setSignedPreKeyFailureCount(context, 0);
ApplicationDependencies.getJobManager().add(new CleanPreKeysJob());
metadataStore.setActiveSignedPreKeyId(signedPreKeyRecord.getId());
metadataStore.setSignedPreKeyRegistered(true);
metadataStore.setSignedPreKeyFailureCount(0);
}
@Override
@ -73,7 +97,11 @@ public class RotateSignedPreKeyJob extends BaseJob {
@Override
public void onFailure() {
TextSecurePreferences.setSignedPreKeyFailureCount(context, TextSecurePreferences.getSignedPreKeyFailureCount(context) + 1);
PreKeyMetadataStore aciStore = SignalStore.account().aciPreKeys();
PreKeyMetadataStore pniStore = SignalStore.account().pniPreKeys();
aciStore.setSignedPreKeyFailureCount(aciStore.getSignedPreKeyFailureCount() + 1);
pniStore.setSignedPreKeyFailureCount(pniStore.getSignedPreKeyFailureCount() + 1);
}
public static final class Factory implements Job.Factory<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.MasterCipher
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
@ -18,9 +19,11 @@ import org.thoughtcrime.securesms.util.Util
import org.whispersystems.libsignal.IdentityKey
import org.whispersystems.libsignal.IdentityKeyPair
import org.whispersystems.libsignal.ecc.Curve
import org.whispersystems.libsignal.util.Medium
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.security.SecureRandom
internal class AccountValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
@ -35,10 +38,22 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
private const val KEY_FCM_TOKEN_LAST_SET_TIME = "account.fcm_token_last_set_time"
private const val KEY_DEVICE_NAME = "account.device_name"
private const val KEY_DEVICE_ID = "account.device_id"
private const val KEY_ACI_IDENTITY_PUBLIC_KEY = "account.aci_identity_public_key"
private const val KEY_ACI_IDENTITY_PRIVATE_KEY = "account.aci_identity_private_key"
private const val KEY_ACI_SIGNED_PREKEY_REGISTERED = "account.aci_signed_prekey_registered"
private const val KEY_ACI_NEXT_SIGNED_PREKEY_ID = "account.aci_next_signed_prekey_id"
private const val KEY_ACI_ACTIVE_SIGNED_PREKEY_ID = "account.aci_active_signed_prekey_id"
private const val KEY_ACI_SIGNED_PREKEY_FAILURE_COUNT = "account.aci_signed_prekey_failure_count"
private const val KEY_ACI_NEXT_ONE_TIME_PREKEY_ID = "account.aci_next_one_time_prekey_id"
private const val KEY_PNI_IDENTITY_PUBLIC_KEY = "account.pni_identity_public_key"
private const val KEY_PNI_IDENTITY_PRIVATE_KEY = "account.pni_identity_private_key"
private const val KEY_PNI_SIGNED_PREKEY_REGISTERED = "account.pni_signed_prekey_registered"
private const val KEY_PNI_NEXT_SIGNED_PREKEY_ID = "account.pni_next_signed_prekey_id"
private const val KEY_PNI_ACTIVE_SIGNED_PREKEY_ID = "account.pni_active_signed_prekey_id"
private const val KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT = "account.pni_signed_prekey_failure_count"
private const val KEY_PNI_NEXT_ONE_TIME_PREKEY_ID = "account.pni_next_one_time_prekey_id"
@VisibleForTesting
const val KEY_E164 = "account.e164"
@ -102,9 +117,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
}
/** A randomly-generated value that represents this registration instance. Helps the server know if you reinstalled. */
var registrationId: Int
get() = getInteger(KEY_REGISTRATION_ID, 0)
set(value) = putInteger(KEY_REGISTRATION_ID, value)
var registrationId: Int by integerValue(KEY_REGISTRATION_ID, 0)
/** The identity key pair for the ACI identity. */
val aciIdentityKey: IdentityKeyPair
@ -147,8 +160,8 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
val key: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
store
.beginWrite()
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize())
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize())
.putBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize())
.putBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize())
.commit()
}
@ -174,11 +187,27 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, Base64.decode(base64))
}
@get:JvmName("aciPreKeys")
val aciPreKeys: PreKeyMetadataStore = object : PreKeyMetadataStore {
override var nextSignedPreKeyId: Int by integerValue(KEY_ACI_NEXT_SIGNED_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE))
override var activeSignedPreKeyId: Int by integerValue(KEY_ACI_ACTIVE_SIGNED_PREKEY_ID, -1)
override var isSignedPreKeyRegistered: Boolean by booleanValue(KEY_ACI_SIGNED_PREKEY_REGISTERED, false)
override var signedPreKeyFailureCount: Int by integerValue(KEY_ACI_SIGNED_PREKEY_FAILURE_COUNT, 0)
override var nextOneTimePreKeyId: Int by integerValue(KEY_ACI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE))
}
@get:JvmName("pniPreKeys")
val pniPreKeys: PreKeyMetadataStore = object : PreKeyMetadataStore {
override var nextSignedPreKeyId: Int by integerValue(KEY_PNI_NEXT_SIGNED_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE))
override var activeSignedPreKeyId: Int by integerValue(KEY_PNI_ACTIVE_SIGNED_PREKEY_ID, -1)
override var isSignedPreKeyRegistered: Boolean by booleanValue(KEY_PNI_SIGNED_PREKEY_REGISTERED, false)
override var signedPreKeyFailureCount: Int by integerValue(KEY_PNI_SIGNED_PREKEY_FAILURE_COUNT, 0)
override var nextOneTimePreKeyId: Int by integerValue(KEY_PNI_NEXT_ONE_TIME_PREKEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE))
}
/** Indicates whether the user has the ability to receive FCM messages. Largely coupled to whether they have Play Service. */
var fcmEnabled: Boolean
@JvmName("isFcmEnabled")
get() = getBoolean(KEY_FCM_ENABLED, false)
set(value) = putBoolean(KEY_FCM_ENABLED, value)
@get:JvmName("isFcmEnabled")
var fcmEnabled: Boolean by booleanValue(KEY_FCM_ENABLED, false)
/** The FCM token, which allows the server to send us FCM messages. */
var fcmToken: String?
@ -249,6 +278,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
ApplicationDependencies.getGroupsV2Authorization().clear()
}
/** Do not alter. If you need to migrate more stuff, create a new method. */
private fun migrateFromSharedPrefsV1(context: Context) {
Log.i(TAG, "[V1] Migrating account values from shared prefs.")
@ -263,23 +293,24 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
putLong(KEY_FCM_TOKEN_LAST_SET_TIME, TextSecurePreferences.getLongPreference(context, "pref_gcm_registration_id_last_set_time", 0))
}
/** Do not alter. If you need to migrate more stuff, create a new method. */
private fun migrateFromSharedPrefsV2(context: Context) {
Log.i(TAG, "[V2] Migrating account values from shared prefs.")
val masterSecretPrefs: SharedPreferences = context.getSharedPreferences("SecureSMS-Preferences", 0)
val defaultPrefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val storeWriter: KeyValueStore.Writer = store.beginWrite()
if (masterSecretPrefs.hasStringData("pref_identity_public_v3")) {
Log.i(TAG, "Migrating modern identity key.")
val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_v3", null)!!)
val identityPrivate = Base64.decode(masterSecretPrefs.getString("pref_identity_private_v3", null)!!)
store
.beginWrite()
storeWriter
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic)
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate)
.commit()
} else if (masterSecretPrefs.hasStringData("pref_identity_public_curve25519")) {
Log.i(TAG, "Migrating legacy identity key.")
@ -287,15 +318,21 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
val identityPublic = Base64.decode(masterSecretPrefs.getString("pref_identity_public_curve25519", null)!!)
val identityPrivate = masterCipher.decryptKey(Base64.decode(masterSecretPrefs.getString("pref_identity_private_curve25519", null)!!)).serialize()
store
.beginWrite()
storeWriter
.putBlob(KEY_ACI_IDENTITY_PUBLIC_KEY, identityPublic)
.putBlob(KEY_ACI_IDENTITY_PRIVATE_KEY, identityPrivate)
.commit()
} else {
Log.w(TAG, "No pre-existing identity key! No migration.")
}
storeWriter
.putInteger(KEY_ACI_NEXT_SIGNED_PREKEY_ID, defaultPrefs.getInt("pref_next_signed_pre_key_id", SecureRandom().nextInt(Medium.MAX_VALUE)))
.putInteger(KEY_ACI_ACTIVE_SIGNED_PREKEY_ID, defaultPrefs.getInt("pref_active_signed_pre_key_id", -1))
.putInteger(KEY_ACI_NEXT_ONE_TIME_PREKEY_ID, defaultPrefs.getInt("pref_next_pre_key_id", SecureRandom().nextInt(Medium.MAX_VALUE)))
.putInteger(KEY_ACI_SIGNED_PREKEY_FAILURE_COUNT, defaultPrefs.getInt("pref_signed_prekey_failure_count", 0))
.putBoolean(KEY_ACI_SIGNED_PREKEY_REGISTERED, defaultPrefs.getBoolean("pref_signed_prekey_registered", false))
.commit()
masterSecretPrefs
.edit()
.remove("pref_identity_public_v3")
@ -308,6 +345,11 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
.edit()
.remove("pref_local_uuid")
.remove("pref_identity_public_v3")
.remove("pref_next_signed_pre_key_id")
.remove("pref_active_signed_pre_key_id")
.remove("pref_signed_prekey_failure_count")
.remove("pref_signed_prekey_registered")
.remove("pref_next_pre_key_id")
.remove("pref_gcm_password")
.remove("pref_gcm_registered")
.remove("pref_local_registration_id")

View file

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

View file

@ -97,9 +97,10 @@ public class ApplicationMigrations {
static final int FIX_EMOJI_QUALITY = 53;
static final int CHANGE_NUMBER_CAPABILITY_4 = 54;
static final int KBS_MIGRATION = 55;
static final int PNI_IDENTITY = 56;
}
public static final int CURRENT_VERSION = 55;
public static final int CURRENT_VERSION = 56;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@ -421,6 +422,10 @@ public class ApplicationMigrations {
jobs.put(Version.KBS_MIGRATION, new KbsEnclaveMigrationJob());
}
if (lastSeenVersion < Version.PNI_IDENTITY) {
jobs.put(Version.PNI_IDENTITY, new PniAccountInitializationMigrationJob());
}
return jobs;
}

View file

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

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.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.crypto.storage.SignalServiceAccountDataStoreImpl;
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
@ -30,14 +32,15 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAcc
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.AccountIdentifier;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.ServiceResponse;
@ -74,7 +77,7 @@ public final class RegistrationRepository {
}
public @NonNull ProfileKey getProfileKey(@NonNull String e164) {
ProfileKey profileKey = findExistingProfileKey(context, e164);
ProfileKey profileKey = findExistingProfileKey(e164);
if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew();
@ -127,19 +130,22 @@ public final class RegistrationRepository {
@Nullable KbsPinData kbsData)
throws IOException
{
SessionUtil.archiveAllSessions();
SenderKeyUtil.clearAllState(context);
ACI aci = ACI.parseOrThrow(response.getUuid());
PNI pni = PNI.parseOrThrow(response.getPni());
boolean hasPin = response.isStorageCapable();
IdentityKeyPair identityKey = SignalStore.account().getAciIdentityKey();
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
SignalStore.account().setAci(aci);
SignalStore.account().setPni(pni);
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword());
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
SessionUtil.archiveAllSessions();
SenderKeyUtil.clearAllState(context);
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword());
SignalServiceAccountDataStoreImpl aciProtocolStore = ApplicationDependencies.getProtocolStore().aci();
SignalServiceAccountDataStoreImpl pniProtocolStore = ApplicationDependencies.getProtocolStore().pni();
generateAndRegisterPreKeys(aci, accountManager, aciProtocolStore, SignalStore.account().aciPreKeys());
generateAndRegisterPreKeys(pni, accountManager, pniProtocolStore, SignalStore.account().pniPreKeys());
if (registrationData.isFcm()) {
accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken()));
@ -151,35 +157,50 @@ public final class RegistrationRepository {
recipientDatabase.setProfileSharing(selfId, true);
recipientDatabase.markRegisteredOrThrow(selfId, aci);
recipientDatabase.setPni(selfId, pni);
SignalStore.account().setE164(registrationData.getE164());
SignalStore.account().setAci(aci);
SignalStore.account().setPni(pni);
recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey());
ApplicationDependencies.getRecipientCache().clearSelf();
SignalStore.account().setE164(registrationData.getE164());
SignalStore.account().setFcmToken(registrationData.getFcmToken());
SignalStore.account().setFcmEnabled(registrationData.isFcm());
ApplicationDependencies.getProtocolStore().aci().identities()
.saveIdentityWithoutSideEffects(selfId,
identityKey.getPublicKey(),
IdentityDatabase.VerifiedStatus.VERIFIED,
true,
System.currentTimeMillis(),
true);
long now = System.currentTimeMillis();
saveOwnIdentityKey(selfId, aciProtocolStore, now);
saveOwnIdentityKey(selfId, pniProtocolStore, now);
SignalStore.account().setServicePassword(registrationData.getPassword());
SignalStore.account().setRegistered(true);
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
PinState.onRegistration(context, kbsData, pin, hasPin);
}
private void generateAndRegisterPreKeys(@NonNull AccountIdentifier accountId,
@NonNull SignalServiceAccountManager accountManager,
@NonNull SignalProtocolStore protocolStore,
@NonNull PreKeyMetadataStore metadataStore)
throws IOException
{
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateAndStoreSignedPreKey(protocolStore, metadataStore, true);
List<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
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
private static @Nullable ProfileKey findExistingProfileKey(@NonNull String e164number) {
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
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";
public static final String MESSAGE_BODY_TEXT_SIZE_PREF = "pref_message_body_text_size";
private static final String SIGNED_PREKEY_REGISTERED_PREF = "pref_signed_prekey_registered";
private static final String WIFI_SMS_PREF = "pref_wifi_sms";
private static final String RATING_LATER_PREF = "pref_rating_later";
private static final String RATING_ENABLED_PREF = "pref_rating_enabled";
private static final String SIGNED_PREKEY_FAILURE_COUNT_PREF = "pref_signed_prekey_failure_count";
public static final String REPEAT_ALERTS_PREF = "pref_repeat_alerts";
public static final String NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy";
@ -127,10 +125,6 @@ public class TextSecurePreferences {
public static final String CALL_RINGTONE_PREF = "pref_call_ringtone";
public static final String CALL_VIBRATE_PREF = "pref_call_vibrate";
private static final String NEXT_PRE_KEY_ID = "pref_next_pre_key_id";
private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id";
private static final String NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id";
public static final String BACKUP = "pref_backup";
public static final String BACKUP_ENABLED = "pref_backup_enabled";
private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase";
@ -400,30 +394,6 @@ public class TextSecurePreferences {
return getLongPreference(context, BACKUP_TIME, -1);
}
public static int getNextPreKeyId(@NonNull Context context) {
return getIntegerPreference(context, NEXT_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE));
}
public static void setNextPreKeyId(@NonNull Context context, int value) {
setIntegerPrefrence(context, NEXT_PRE_KEY_ID, value);
}
public static int getNextSignedPreKeyId(@NonNull Context context) {
return getIntegerPreference(context, NEXT_SIGNED_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE));
}
public static void setNextSignedPreKeyId(@NonNull Context context, int value) {
setIntegerPrefrence(context, NEXT_SIGNED_PRE_KEY_ID, value);
}
public static int getActiveSignedPreKeyId(@NonNull Context context) {
return getIntegerPreference(context, ACTIVE_SIGNED_PRE_KEY_ID, -1);
}
public static void setActiveSignedPreKeyId(@NonNull Context context, int value) {
setIntegerPrefrence(context, ACTIVE_SIGNED_PRE_KEY_ID, value);;
}
public static void setNeedsSqlCipherMigration(@NonNull Context context, boolean value) {
setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value);
EventBus.getDefault().post(new SqlCipherMigrationConstraintObserver.SqlCipherNeedsMigrationEvent());
@ -563,14 +533,6 @@ public class TextSecurePreferences {
return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false);
}
public static void setSignedPreKeyFailureCount(Context context, int value) {
setIntegerPrefrence(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, value);
}
public static int getSignedPreKeyFailureCount(Context context) {
return getIntegerPreference(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, 0);
}
@Deprecated
public static NotificationPrivacyPreference getNotificationPrivacy(Context context) {
return new NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all"));
@ -611,14 +573,6 @@ public class TextSecurePreferences {
}
}
public static boolean isSignedPreKeyRegistered(Context context) {
return getBooleanPreference(context, SIGNED_PREKEY_REGISTERED_PREF, false);
}
public static void setSignedPreKeyRegistered(Context context, boolean value) {
setBooleanPreference(context, SIGNED_PREKEY_REGISTERED_PREF, value);
}
@Deprecated
public static boolean isInThreadNotifications(Context context) {
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);

View file

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

View file

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

View file

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

View file

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

View file

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