Migrate the identity table to be keyed off of libsignal IDs.
This commit is contained in:
parent
2068fa8041
commit
dbb1e50d00
6 changed files with 236 additions and 126 deletions
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.crypto.storage;
|
|||
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;
|
||||
|
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityStoreRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
|
@ -49,12 +51,12 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
|||
public @NonNull SaveResult saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
|
||||
synchronized (LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(address.getName());
|
||||
RecipientId recipientId = RecipientId.fromExternalPush(address.getName());
|
||||
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.i(TAG, "Saving new identity...");
|
||||
identityDatabase.saveIdentity(recipientId, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
|
||||
identityDatabase.saveIdentity(address.getName(), recipientId, identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
|
||||
return SaveResult.NEW;
|
||||
}
|
||||
|
||||
|
@ -70,7 +72,8 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
|||
verifiedStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
||||
identityDatabase.saveIdentity(recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
|
||||
|
||||
identityDatabase.saveIdentity(address.getName(), recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
|
||||
IdentityUtil.markIdentityUpdate(context, recipientId);
|
||||
SessionUtil.archiveSiblingSessions(context, address);
|
||||
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId);
|
||||
|
@ -95,65 +98,49 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
|||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
synchronized (LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
RecipientId ourRecipientId = Recipient.self().getId();
|
||||
RecipientId theirRecipientId = RecipientId.fromExternalPush(address.getName());
|
||||
boolean isSelf = address.getName().equals(TextSecurePreferences.getLocalUuid(context).toString()) ||
|
||||
address.getName().equals(TextSecurePreferences.getLocalNumber(context));
|
||||
|
||||
if (ourRecipientId.equals(theirRecipientId)) {
|
||||
if (isSelf) {
|
||||
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
|
||||
}
|
||||
|
||||
IdentityStoreRecord record = DatabaseFactory.getIdentityDatabase(context).getIdentityStoreRecord(address.getName());
|
||||
|
||||
switch (direction) {
|
||||
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(theirRecipientId));
|
||||
case RECEIVING: return true;
|
||||
default: throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Tried to check if identity is trusted for " + address.getName() + ", but no matching recipient existed!");
|
||||
switch (direction) {
|
||||
case SENDING: return false;
|
||||
case RECEIVING: return true;
|
||||
default: throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
case SENDING:
|
||||
return isTrustedForSending(identityKey, record);
|
||||
case RECEIVING:
|
||||
return true;
|
||||
default:
|
||||
throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
RecipientId recipientId = RecipientId.fromExternalPush(address.getName());
|
||||
Optional<IdentityRecord> record = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipientId);
|
||||
|
||||
if (record.isPresent()) {
|
||||
return record.get().getIdentityKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Tried to get identity for " + address.getName() + ", but no matching recipient existed!");
|
||||
return null;
|
||||
}
|
||||
IdentityStoreRecord record = DatabaseFactory.getIdentityDatabase(context).getIdentityStoreRecord(address.getName());
|
||||
return record != null ? record.getIdentityKey() : null;
|
||||
}
|
||||
|
||||
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
|
||||
if (!identityRecord.isPresent()) {
|
||||
private boolean isTrustedForSending(@NonNull IdentityKey identityKey, @Nullable IdentityStoreRecord identityRecord) {
|
||||
if (identityRecord == null) {
|
||||
Log.w(TAG, "Nothing here, returning true...");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
|
||||
Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.get().getIdentityKey().hashCode());
|
||||
if (!identityKey.equals(identityRecord.getIdentityKey())) {
|
||||
Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.getIdentityKey().hashCode());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
Log.w(TAG, "Needs unverified approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
if (isNonBlockingApprovalRequired(identityRecord)) {
|
||||
Log.w(TAG, "Needs non-blocking approval!");
|
||||
return false;
|
||||
}
|
||||
|
@ -162,9 +149,17 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
|||
}
|
||||
|
||||
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
|
||||
return !identityRecord.isFirstUse() &&
|
||||
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
|
||||
!identityRecord.isApprovedNonBlocking();
|
||||
return isNonBlockingApprovalRequired(identityRecord.isFirstUse(), identityRecord.getTimestamp(), identityRecord.isApprovedNonBlocking());
|
||||
}
|
||||
|
||||
private boolean isNonBlockingApprovalRequired(IdentityStoreRecord identityRecord) {
|
||||
return isNonBlockingApprovalRequired(identityRecord.getFirstUse(), identityRecord.getTimestamp(), identityRecord.getNonblockingApproval());
|
||||
}
|
||||
|
||||
private boolean isNonBlockingApprovalRequired(boolean firstUse, long timestamp, boolean nonblockingApproval) {
|
||||
return !firstUse &&
|
||||
!nonblockingApproval &&
|
||||
System.currentTimeMillis() - timestamp < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS);
|
||||
}
|
||||
|
||||
public enum SaveResult {
|
||||
|
|
|
@ -27,10 +27,13 @@ import org.greenrobot.eventbus.EventBus;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityStoreRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -38,6 +41,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IdentityDatabase extends Database {
|
||||
|
||||
|
@ -46,16 +50,15 @@ public class IdentityDatabase extends Database {
|
|||
|
||||
static final String TABLE_NAME = "identities";
|
||||
private static final String ID = "_id";
|
||||
static final String RECIPIENT_ID = "address";
|
||||
static final String IDENTITY_KEY = "key";
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
static final String ADDRESS = "address";
|
||||
static final String IDENTITY_KEY = "identity_key";
|
||||
private static final String FIRST_USE = "first_use";
|
||||
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
|
||||
private static final String TIMESTAMP = "timestamp";
|
||||
static final String VERIFIED = "verified";
|
||||
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
RECIPIENT_ID + " INTEGER UNIQUE, " +
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
ADDRESS + " INTEGER UNIQUE, " +
|
||||
IDENTITY_KEY + " TEXT, " +
|
||||
FIRST_USE + " INTEGER DEFAULT 0, " +
|
||||
TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
|
@ -66,17 +69,21 @@ public class IdentityDatabase extends Database {
|
|||
DEFAULT, VERIFIED, UNVERIFIED;
|
||||
|
||||
public int toInt() {
|
||||
if (this == DEFAULT) return 0;
|
||||
else if (this == VERIFIED) return 1;
|
||||
else if (this == UNVERIFIED) return 2;
|
||||
else throw new AssertionError();
|
||||
switch (this) {
|
||||
case DEFAULT: return 0;
|
||||
case VERIFIED: return 1;
|
||||
case UNVERIFIED: return 2;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static VerifiedStatus forState(int state) {
|
||||
if (state == 0) return DEFAULT;
|
||||
else if (state == 1) return VERIFIED;
|
||||
else if (state == 2) return UNVERIFIED;
|
||||
else throw new AssertionError("No such state: " + state);
|
||||
switch (state) {
|
||||
case 0: return DEFAULT;
|
||||
case 1: return VERIFIED;
|
||||
case 2: return UNVERIFIED;
|
||||
default: throw new AssertionError("No such state: " + state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,55 +101,108 @@ public class IdentityDatabase extends Database {
|
|||
return new IdentityReader(cursor);
|
||||
}
|
||||
|
||||
public Optional<IdentityRecord> getIdentity(@NonNull RecipientId recipientId) {
|
||||
public Optional<IdentityRecord> getIdentity(@NonNull String addressName) {
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
String query = ADDRESS + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(addressName);
|
||||
|
||||
try {
|
||||
cursor = database.query(TABLE_NAME, null, RECIPIENT_ID + " = ?",
|
||||
new String[] {recipientId.serialize()}, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return Optional.of(getIdentityRecord(cursor));
|
||||
}
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public @NonNull IdentityRecordList getIdentities(@NonNull List<Recipient> recipients) {
|
||||
List<IdentityRecord> records = new LinkedList<>();
|
||||
public Optional<IdentityRecord> getIdentity(@NonNull RecipientId recipientId) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (recipient.hasServiceIdentifier()) {
|
||||
return getIdentity(recipient.requireServiceId());
|
||||
} else {
|
||||
Log.w(TAG, "Recipient has no service identifier!");
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable IdentityStoreRecord getIdentityStoreRecord(@NonNull String addressName) {
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
String[] selectionArgs = new String[1];
|
||||
String query = ADDRESS + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(addressName);
|
||||
|
||||
database.beginTransaction();
|
||||
try {
|
||||
for (Recipient recipient : recipients) {
|
||||
selectionArgs[0] = recipient.getId().serialize();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, RECIPIENT_ID + " = ?", selectionArgs, null, null, null)) {
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
records.add(getIdentityRecord(cursor));
|
||||
String serializedIdentity = CursorUtil.requireString(cursor, IDENTITY_KEY);
|
||||
long timestamp = CursorUtil.requireLong(cursor, TIMESTAMP);
|
||||
int verifiedStatus = CursorUtil.requireInt(cursor, VERIFIED);
|
||||
boolean nonblockingApproval = CursorUtil.requireBoolean(cursor, NONBLOCKING_APPROVAL);
|
||||
boolean firstUse = CursorUtil.requireBoolean(cursor, FIRST_USE);
|
||||
|
||||
return new IdentityStoreRecord(addressName,
|
||||
new IdentityKey(Base64.decode(serializedIdentity), 0),
|
||||
VerifiedStatus.forState(verifiedStatus),
|
||||
firstUse,
|
||||
timestamp,
|
||||
nonblockingApproval);
|
||||
}
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull IdentityRecordList getIdentities(@NonNull List<Recipient> recipients) {
|
||||
List<String> addressNames = recipients.stream()
|
||||
.filter(Recipient::hasServiceIdentifier)
|
||||
.map(Recipient::requireServiceId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (addressNames.isEmpty()) {
|
||||
return IdentityRecordList.EMPTY;
|
||||
}
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||
SqlUtil.Query query = SqlUtil.buildCollectionQuery(ADDRESS, addressNames);
|
||||
|
||||
List<IdentityRecord> records = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, null, query.getWhere(), query.getWhereArgs(), null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
records.add(getIdentityRecord(cursor));
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
|
||||
return new IdentityRecordList(records);
|
||||
}
|
||||
|
||||
public void saveIdentity(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus,
|
||||
boolean firstUse, long timestamp, boolean nonBlockingApproval)
|
||||
public void saveIdentity(@NonNull String addressName,
|
||||
@NonNull RecipientId recipientId,
|
||||
IdentityKey identityKey,
|
||||
VerifiedStatus verifiedStatus,
|
||||
boolean firstUse,
|
||||
long timestamp,
|
||||
boolean nonBlockingApproval)
|
||||
{
|
||||
saveIdentityInternal(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
|
||||
saveIdentityInternal(addressName, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
|
||||
}
|
||||
|
||||
public void saveIdentity(@NonNull RecipientId recipientId,
|
||||
IdentityKey identityKey,
|
||||
VerifiedStatus verifiedStatus,
|
||||
boolean firstUse,
|
||||
long timestamp,
|
||||
boolean nonBlockingApproval)
|
||||
{
|
||||
saveIdentityInternal(Recipient.resolved(recipientId).requireServiceId(), identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
|
||||
}
|
||||
|
||||
|
@ -152,7 +212,7 @@ public class IdentityDatabase extends Database {
|
|||
ContentValues contentValues = new ContentValues(2);
|
||||
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
|
||||
|
||||
database.update(TABLE_NAME, contentValues, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()});
|
||||
database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", SqlUtil.buildArgs(Recipient.resolved(recipientId).requireServiceId()));
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
|
||||
}
|
||||
|
@ -160,11 +220,13 @@ public class IdentityDatabase extends Database {
|
|||
public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
||||
|
||||
String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(Recipient.resolved(recipientId).requireServiceId(), Base64.encodeBytes(identityKey.serialize()));
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(VERIFIED, verifiedStatus.toInt());
|
||||
|
||||
int updated = database.update(TABLE_NAME, contentValues, RECIPIENT_ID + " = ? AND " + IDENTITY_KEY + " = ?",
|
||||
new String[] {recipientId.serialize(), Base64.encodeBytes(identityKey.serialize())});
|
||||
int updated = database.update(TABLE_NAME, contentValues, query, args);
|
||||
|
||||
if (updated > 0) {
|
||||
Optional<IdentityRecord> record = getIdentity(recipientId);
|
||||
|
@ -173,36 +235,40 @@ public class IdentityDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void updateIdentityAfterSync(@NonNull RecipientId id, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
boolean hadEntry = getIdentity(id).isPresent();
|
||||
boolean keyMatches = hasMatchingKey(id, identityKey);
|
||||
boolean statusMatches = keyMatches && hasMatchingStatus(id, identityKey, verifiedStatus);
|
||||
public void updateIdentityAfterSync(@NonNull String addressName, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
boolean hadEntry = getIdentity(addressName).isPresent();
|
||||
boolean keyMatches = hasMatchingKey(addressName, identityKey);
|
||||
boolean statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus);
|
||||
|
||||
if (!keyMatches || !statusMatches) {
|
||||
saveIdentityInternal(id, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true);
|
||||
Optional<IdentityRecord> record = getIdentity(id);
|
||||
if (record.isPresent()) EventBus.getDefault().post(record.get());
|
||||
saveIdentityInternal(addressName, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true);
|
||||
|
||||
Optional<IdentityRecord> record = getIdentity(addressName);
|
||||
|
||||
if (record.isPresent()) {
|
||||
EventBus.getDefault().post(record.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (hadEntry && !keyMatches) {
|
||||
IdentityUtil.markIdentityUpdate(context, id);
|
||||
IdentityUtil.markIdentityUpdate(context, RecipientId.fromExternalPush(addressName));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMatchingKey(@NonNull RecipientId id, IdentityKey identityKey) {
|
||||
private boolean hasMatchingKey(@NonNull String addressName, IdentityKey identityKey) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String query = RECIPIENT_ID + " = ? AND " + IDENTITY_KEY + " = ?";
|
||||
String[] args = new String[]{id.serialize(), Base64.encodeBytes(identityKey.serialize())};
|
||||
String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()));
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
return cursor != null && cursor.moveToFirst();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMatchingStatus(@NonNull RecipientId id, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
private boolean hasMatchingStatus(@NonNull String addressName, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String query = RECIPIENT_ID + " = ? AND " + IDENTITY_KEY + " = ? AND " + VERIFIED + " = ?";
|
||||
String[] args = new String[]{id.serialize(), Base64.encodeBytes(identityKey.serialize()), String.valueOf(verifiedStatus.toInt())};
|
||||
String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ? AND " + VERIFIED + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()), verifiedStatus.toInt());
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
return cursor != null && cursor.moveToFirst();
|
||||
|
@ -210,25 +276,29 @@ public class IdentityDatabase extends Database {
|
|||
}
|
||||
|
||||
private static @NonNull IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException {
|
||||
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID));
|
||||
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP));
|
||||
int verifiedStatus = cursor.getInt(cursor.getColumnIndexOrThrow(VERIFIED));
|
||||
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1;
|
||||
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1;
|
||||
String addressName = CursorUtil.requireString(cursor, ADDRESS);
|
||||
String serializedIdentity = CursorUtil.requireString(cursor, IDENTITY_KEY);
|
||||
long timestamp = CursorUtil.requireLong(cursor, TIMESTAMP);
|
||||
int verifiedStatus = CursorUtil.requireInt(cursor, VERIFIED);
|
||||
boolean nonblockingApproval = CursorUtil.requireBoolean(cursor, NONBLOCKING_APPROVAL);
|
||||
boolean firstUse = CursorUtil.requireBoolean(cursor, FIRST_USE);
|
||||
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0);
|
||||
|
||||
return new IdentityRecord(RecipientId.from(recipientId), identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval);
|
||||
return new IdentityRecord(RecipientId.fromExternalPush(addressName), identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval);
|
||||
}
|
||||
|
||||
private void saveIdentityInternal(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus,
|
||||
boolean firstUse, long timestamp, boolean nonBlockingApproval)
|
||||
private void saveIdentityInternal(@NonNull String addressName,
|
||||
IdentityKey identityKey,
|
||||
VerifiedStatus verifiedStatus,
|
||||
boolean firstUse,
|
||||
long timestamp,
|
||||
boolean nonBlockingApproval)
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
||||
String identityKeyString = Base64.encodeBytes(identityKey.serialize());
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(RECIPIENT_ID, recipientId.serialize());
|
||||
contentValues.put(ADDRESS, addressName);
|
||||
contentValues.put(IDENTITY_KEY, identityKeyString);
|
||||
contentValues.put(TIMESTAMP, timestamp);
|
||||
contentValues.put(VERIFIED, verifiedStatus.toInt());
|
||||
|
@ -237,8 +307,7 @@ public class IdentityDatabase extends Database {
|
|||
|
||||
database.replace(TABLE_NAME, null, contentValues);
|
||||
|
||||
EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus,
|
||||
firstUse, timestamp, nonBlockingApproval));
|
||||
EventBus.getDefault().post(new IdentityRecord(RecipientId.fromExternalPush(addressName), identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval));
|
||||
}
|
||||
|
||||
public static class IdentityRecord {
|
||||
|
@ -251,8 +320,11 @@ public class IdentityDatabase extends Database {
|
|||
private final boolean nonblockingApproval;
|
||||
|
||||
private IdentityRecord(@NonNull RecipientId recipientId,
|
||||
IdentityKey identitykey, VerifiedStatus verifiedStatus,
|
||||
boolean firstUse, long timestamp, boolean nonblockingApproval)
|
||||
IdentityKey identitykey,
|
||||
VerifiedStatus verifiedStatus,
|
||||
boolean firstUse,
|
||||
long timestamp,
|
||||
boolean nonblockingApproval)
|
||||
{
|
||||
this.recipientId = recipientId;
|
||||
this.identitykey = identitykey;
|
||||
|
@ -293,7 +365,7 @@ public class IdentityDatabase extends Database {
|
|||
|
||||
}
|
||||
|
||||
public class IdentityReader {
|
||||
public static class IdentityReader {
|
||||
private final Cursor cursor;
|
||||
|
||||
IdentityReader(@NonNull Cursor cursor) {
|
||||
|
|
|
@ -802,7 +802,7 @@ public class RecipientDatabase extends Database {
|
|||
try {
|
||||
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(insert.getAddress().getIdentifier(), identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
|
||||
}
|
||||
|
@ -846,7 +846,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
if (update.getNew().getIdentityKey().isPresent()) {
|
||||
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0);
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(update.getNew().getAddress().getIdentifier(), identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
|
||||
}
|
||||
|
||||
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
@ -1093,7 +1093,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
|
||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + UUID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.ADDRESS
|
||||
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID
|
||||
+ " LEFT OUTER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
|
||||
List<RecipientSettings> out = new ArrayList<>();
|
||||
|
@ -2913,7 +2913,7 @@ public class RecipientDatabase extends Database {
|
|||
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid));
|
||||
|
||||
// Identities
|
||||
db.delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164));
|
||||
db.delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.ADDRESS + " = ?", SqlUtil.buildArgs(byE164));
|
||||
|
||||
// Group Receipts
|
||||
ContentValues groupReceiptValues = new ContentValues();
|
||||
|
|
|
@ -211,8 +211,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
private static final int AVATAR_PICKER = 111;
|
||||
private static final int THREAD_CLEANUP = 112;
|
||||
private static final int SESSION_MIGRATION = 113;
|
||||
private static final int IDENTITY_MIGRATION = 114;
|
||||
|
||||
private static final int DATABASE_VERSION = 113;
|
||||
private static final int DATABASE_VERSION = 114;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -1988,6 +1989,33 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
Log.d(TAG, "Session migration took " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
|
||||
if (oldVersion < IDENTITY_MIGRATION) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
db.execSQL("CREATE TABLE identities_tmp (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"address TEXT UNIQUE NOT NULL, " +
|
||||
"identity_key TEXT, " +
|
||||
"first_use INTEGER DEFAULT 0, " +
|
||||
"timestamp INTEGER DEFAULT 0, " +
|
||||
"verified INTEGER DEFAULT 0, " +
|
||||
"nonblocking_approval INTEGER DEFAULT 0)");
|
||||
|
||||
db.execSQL("INSERT INTO identities_tmp (address, identity_key, first_use, timestamp, verified, nonblocking_approval) " +
|
||||
"SELECT COALESCE(recipient.uuid, recipient.phone) AS new_address, " +
|
||||
"identities.key, " +
|
||||
"identities.first_use, " +
|
||||
"identities.timestamp, " +
|
||||
"identities.verified, " +
|
||||
"identities.nonblocking_approval " +
|
||||
"FROM identities INNER JOIN recipient ON identities.address = recipient._id " +
|
||||
"WHERE new_address NOT NULL");
|
||||
|
||||
db.execSQL("DROP TABLE identities");
|
||||
db.execSQL("ALTER TABLE identities_tmp RENAME TO identities");
|
||||
|
||||
Log.d(TAG, "Identity migration took " + (System.currentTimeMillis() - start) + " ms");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -14,6 +14,8 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
public final class IdentityRecordList {
|
||||
|
||||
public static final IdentityRecordList EMPTY = new IdentityRecordList(Collections.emptyList());
|
||||
|
||||
private final List<IdentityRecord> identityRecords;
|
||||
private final boolean isVerified;
|
||||
private final boolean isUnverified;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.whispersystems.libsignal.IdentityKey
|
||||
|
||||
data class IdentityStoreRecord(
|
||||
val addressName: String,
|
||||
val identityKey: IdentityKey,
|
||||
val verifiedStatus: IdentityDatabase.VerifiedStatus,
|
||||
val firstUse: Boolean,
|
||||
val timestamp: Long,
|
||||
val nonblockingApproval: Boolean
|
||||
)
|
Loading…
Add table
Reference in a new issue