Migrate the identity table to be keyed off of libsignal IDs.

fork-5.53.8
Greyson Parrelli 2021-08-23 11:51:50 -04:00 zatwierdzone przez Alex Hart
rodzic 2068fa8041
commit dbb1e50d00
6 zmienionych plików z 236 dodań i 126 usunięć

Wyświetl plik

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; 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.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.IdentityUtil; 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) { public @NonNull SaveResult saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
synchronized (LOCK) { synchronized (LOCK) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(address.getName());
RecipientId recipientId = RecipientId.fromExternalPush(address.getName()); RecipientId recipientId = RecipientId.fromExternalPush(address.getName());
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipientId);
if (!identityRecord.isPresent()) { if (!identityRecord.isPresent()) {
Log.i(TAG, "Saving new identity..."); 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; return SaveResult.NEW;
} }
@ -70,7 +72,8 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
verifiedStatus = VerifiedStatus.DEFAULT; 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); IdentityUtil.markIdentityUpdate(context, recipientId);
SessionUtil.archiveSiblingSessions(context, address); SessionUtil.archiveSiblingSessions(context, address);
DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId); DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId);
@ -95,65 +98,49 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
@Override @Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
synchronized (LOCK) { synchronized (LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) { boolean isSelf = address.getName().equals(TextSecurePreferences.getLocalUuid(context).toString()) ||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); address.getName().equals(TextSecurePreferences.getLocalNumber(context));
RecipientId ourRecipientId = Recipient.self().getId();
RecipientId theirRecipientId = RecipientId.fromExternalPush(address.getName());
if (ourRecipientId.equals(theirRecipientId)) { if (isSelf) {
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context)); return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
} }
switch (direction) { IdentityStoreRecord record = DatabaseFactory.getIdentityDatabase(context).getIdentityStoreRecord(address.getName());
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(theirRecipientId));
case RECEIVING: return true; switch (direction) {
default: throw new AssertionError("Unknown direction: " + direction); case SENDING:
} return isTrustedForSending(identityKey, record);
} else { case RECEIVING:
Log.w(TAG, "Tried to check if identity is trusted for " + address.getName() + ", but no matching recipient existed!"); return true;
switch (direction) { default:
case SENDING: return false; throw new AssertionError("Unknown direction: " + direction);
case RECEIVING: return true;
default: throw new AssertionError("Unknown direction: " + direction);
}
} }
} }
} }
@Override @Override
public IdentityKey getIdentity(SignalProtocolAddress address) { public IdentityKey getIdentity(SignalProtocolAddress address) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) { IdentityStoreRecord record = DatabaseFactory.getIdentityDatabase(context).getIdentityStoreRecord(address.getName());
RecipientId recipientId = RecipientId.fromExternalPush(address.getName()); return record != null ? record.getIdentityKey() : null;
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;
}
} }
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) { private boolean isTrustedForSending(@NonNull IdentityKey identityKey, @Nullable IdentityStoreRecord identityRecord) {
if (!identityRecord.isPresent()) { if (identityRecord == null) {
Log.w(TAG, "Nothing here, returning true..."); Log.w(TAG, "Nothing here, returning true...");
return true; return true;
} }
if (!identityKey.equals(identityRecord.get().getIdentityKey())) { if (!identityKey.equals(identityRecord.getIdentityKey())) {
Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.get().getIdentityKey().hashCode()); Log.w(TAG, "Identity keys don't match... service: " + identityKey.hashCode() + " database: " + identityRecord.getIdentityKey().hashCode());
return false; return false;
} }
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) { if (identityRecord.getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
Log.w(TAG, "Needs unverified approval!"); Log.w(TAG, "Needs unverified approval!");
return false; return false;
} }
if (isNonBlockingApprovalRequired(identityRecord.get())) { if (isNonBlockingApprovalRequired(identityRecord)) {
Log.w(TAG, "Needs non-blocking approval!"); Log.w(TAG, "Needs non-blocking approval!");
return false; return false;
} }
@ -162,9 +149,17 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore {
} }
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) { private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
return !identityRecord.isFirstUse() && return isNonBlockingApprovalRequired(identityRecord.isFirstUse(), identityRecord.getTimestamp(), identityRecord.isApprovedNonBlocking());
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) && }
!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 { public enum SaveResult {

Wyświetl plik

@ -27,10 +27,13 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList; 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.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -38,6 +41,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
public class IdentityDatabase extends Database { public class IdentityDatabase extends Database {
@ -46,37 +50,40 @@ public class IdentityDatabase extends Database {
static final String TABLE_NAME = "identities"; static final String TABLE_NAME = "identities";
private static final String ID = "_id"; private static final String ID = "_id";
static final String RECIPIENT_ID = "address"; static final String ADDRESS = "address";
static final String IDENTITY_KEY = "key"; static final String IDENTITY_KEY = "identity_key";
private static final String TIMESTAMP = "timestamp";
private static final String FIRST_USE = "first_use"; 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"; static final String VERIFIED = "verified";
private static final String NONBLOCKING_APPROVAL = "nonblocking_approval";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
" (" + ID + " INTEGER PRIMARY KEY, " + ADDRESS + " INTEGER UNIQUE, " +
RECIPIENT_ID + " INTEGER UNIQUE, " + IDENTITY_KEY + " TEXT, " +
IDENTITY_KEY + " TEXT, " + FIRST_USE + " INTEGER DEFAULT 0, " +
FIRST_USE + " INTEGER DEFAULT 0, " + TIMESTAMP + " INTEGER DEFAULT 0, " +
TIMESTAMP + " INTEGER DEFAULT 0, " + VERIFIED + " INTEGER DEFAULT 0, " +
VERIFIED + " INTEGER DEFAULT 0, " + NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);";
public enum VerifiedStatus { public enum VerifiedStatus {
DEFAULT, VERIFIED, UNVERIFIED; DEFAULT, VERIFIED, UNVERIFIED;
public int toInt() { public int toInt() {
if (this == DEFAULT) return 0; switch (this) {
else if (this == VERIFIED) return 1; case DEFAULT: return 0;
else if (this == UNVERIFIED) return 2; case VERIFIED: return 1;
else throw new AssertionError(); case UNVERIFIED: return 2;
default: throw new AssertionError();
}
} }
public static VerifiedStatus forState(int state) { public static VerifiedStatus forState(int state) {
if (state == 0) return DEFAULT; switch (state) {
else if (state == 1) return VERIFIED; case 0: return DEFAULT;
else if (state == 2) return UNVERIFIED; case 1: return VERIFIED;
else throw new AssertionError("No such state: " + state); 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); return new IdentityReader(cursor);
} }
public Optional<IdentityRecord> getIdentity(@NonNull RecipientId recipientId) { public Optional<IdentityRecord> getIdentity(@NonNull String addressName) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
Cursor cursor = null; String query = ADDRESS + " = ?";
String[] args = SqlUtil.buildArgs(addressName);
try { try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) {
cursor = database.query(TABLE_NAME, null, RECIPIENT_ID + " = ?", if (cursor.moveToFirst()) {
new String[] {recipientId.serialize()}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return Optional.of(getIdentityRecord(cursor)); return Optional.of(getIdentityRecord(cursor));
} }
} catch (InvalidKeyException | IOException e) { } catch (InvalidKeyException | IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
} finally {
if (cursor != null) cursor.close();
} }
return Optional.absent(); return Optional.absent();
} }
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 query = ADDRESS + " = ?";
String[] args = SqlUtil.buildArgs(addressName);
try (Cursor cursor = database.query(TABLE_NAME, null, query, args, null, null, null)) {
if (cursor.moveToFirst()) {
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) { public @NonNull IdentityRecordList getIdentities(@NonNull List<Recipient> recipients) {
List<IdentityRecord> records = new LinkedList<>(); List<String> addressNames = recipients.stream()
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); .filter(Recipient::hasServiceIdentifier)
String[] selectionArgs = new String[1]; .map(Recipient::requireServiceId)
.collect(Collectors.toList());
database.beginTransaction(); if (addressNames.isEmpty()) {
try { return IdentityRecordList.EMPTY;
for (Recipient recipient : recipients) { }
selectionArgs[0] = recipient.getId().serialize();
try (Cursor cursor = database.query(TABLE_NAME, null, RECIPIENT_ID + " = ?", selectionArgs, null, null, null)) { SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
if (cursor.moveToFirst()) { SqlUtil.Query query = SqlUtil.buildCollectionQuery(ADDRESS, addressNames);
records.add(getIdentityRecord(cursor));
} 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) { } catch (InvalidKeyException | IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
} finally {
database.endTransaction();
} }
return new IdentityRecordList(records); return new IdentityRecordList(records);
} }
public void saveIdentity(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus, public void saveIdentity(@NonNull String addressName,
boolean firstUse, long timestamp, boolean nonBlockingApproval) @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); DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
} }
@ -152,7 +212,7 @@ public class IdentityDatabase extends Database {
ContentValues contentValues = new ContentValues(2); ContentValues contentValues = new ContentValues(2);
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval); 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); DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
} }
@ -160,11 +220,13 @@ public class IdentityDatabase extends Database {
public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) { public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); 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 contentValues = new ContentValues(1);
contentValues.put(VERIFIED, verifiedStatus.toInt()); contentValues.put(VERIFIED, verifiedStatus.toInt());
int updated = database.update(TABLE_NAME, contentValues, RECIPIENT_ID + " = ? AND " + IDENTITY_KEY + " = ?", int updated = database.update(TABLE_NAME, contentValues, query, args);
new String[] {recipientId.serialize(), Base64.encodeBytes(identityKey.serialize())});
if (updated > 0) { if (updated > 0) {
Optional<IdentityRecord> record = getIdentity(recipientId); Optional<IdentityRecord> record = getIdentity(recipientId);
@ -173,36 +235,40 @@ public class IdentityDatabase extends Database {
} }
} }
public void updateIdentityAfterSync(@NonNull RecipientId id, IdentityKey identityKey, VerifiedStatus verifiedStatus) { public void updateIdentityAfterSync(@NonNull String addressName, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
boolean hadEntry = getIdentity(id).isPresent(); boolean hadEntry = getIdentity(addressName).isPresent();
boolean keyMatches = hasMatchingKey(id, identityKey); boolean keyMatches = hasMatchingKey(addressName, identityKey);
boolean statusMatches = keyMatches && hasMatchingStatus(id, identityKey, verifiedStatus); boolean statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus);
if (!keyMatches || !statusMatches) { if (!keyMatches || !statusMatches) {
saveIdentityInternal(id, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true); saveIdentityInternal(addressName, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true);
Optional<IdentityRecord> record = getIdentity(id);
if (record.isPresent()) EventBus.getDefault().post(record.get()); Optional<IdentityRecord> record = getIdentity(addressName);
if (record.isPresent()) {
EventBus.getDefault().post(record.get());
}
} }
if (hadEntry && !keyMatches) { 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(); SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String query = RECIPIENT_ID + " = ? AND " + IDENTITY_KEY + " = ?"; String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?";
String[] args = new String[]{id.serialize(), Base64.encodeBytes(identityKey.serialize())}; String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()));
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) { try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) {
return cursor != null && cursor.moveToFirst(); 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(); SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String query = RECIPIENT_ID + " = ? AND " + IDENTITY_KEY + " = ? AND " + VERIFIED + " = ?"; String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ? AND " + VERIFIED + " = ?";
String[] args = new String[]{id.serialize(), Base64.encodeBytes(identityKey.serialize()), String.valueOf(verifiedStatus.toInt())}; String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()), verifiedStatus.toInt());
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) { try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) {
return cursor != null && cursor.moveToFirst(); 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 { private static @NonNull IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException {
long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)); String addressName = CursorUtil.requireString(cursor, ADDRESS);
String serializedIdentity = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY)); String serializedIdentity = CursorUtil.requireString(cursor, IDENTITY_KEY);
long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); long timestamp = CursorUtil.requireLong(cursor, TIMESTAMP);
int verifiedStatus = cursor.getInt(cursor.getColumnIndexOrThrow(VERIFIED)); int verifiedStatus = CursorUtil.requireInt(cursor, VERIFIED);
boolean nonblockingApproval = cursor.getInt(cursor.getColumnIndexOrThrow(NONBLOCKING_APPROVAL)) == 1; boolean nonblockingApproval = CursorUtil.requireBoolean(cursor, NONBLOCKING_APPROVAL);
boolean firstUse = cursor.getInt(cursor.getColumnIndexOrThrow(FIRST_USE)) == 1; boolean firstUse = CursorUtil.requireBoolean(cursor, FIRST_USE);
IdentityKey identity = new IdentityKey(Base64.decode(serializedIdentity), 0); 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, private void saveIdentityInternal(@NonNull String addressName,
boolean firstUse, long timestamp, boolean nonBlockingApproval) IdentityKey identityKey,
VerifiedStatus verifiedStatus,
boolean firstUse,
long timestamp,
boolean nonBlockingApproval)
{ {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
String identityKeyString = Base64.encodeBytes(identityKey.serialize()); String identityKeyString = Base64.encodeBytes(identityKey.serialize());
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(RECIPIENT_ID, recipientId.serialize()); contentValues.put(ADDRESS, addressName);
contentValues.put(IDENTITY_KEY, identityKeyString); contentValues.put(IDENTITY_KEY, identityKeyString);
contentValues.put(TIMESTAMP, timestamp); contentValues.put(TIMESTAMP, timestamp);
contentValues.put(VERIFIED, verifiedStatus.toInt()); contentValues.put(VERIFIED, verifiedStatus.toInt());
@ -237,8 +307,7 @@ public class IdentityDatabase extends Database {
database.replace(TABLE_NAME, null, contentValues); database.replace(TABLE_NAME, null, contentValues);
EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus, EventBus.getDefault().post(new IdentityRecord(RecipientId.fromExternalPush(addressName), identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval));
firstUse, timestamp, nonBlockingApproval));
} }
public static class IdentityRecord { public static class IdentityRecord {
@ -251,8 +320,11 @@ public class IdentityDatabase extends Database {
private final boolean nonblockingApproval; private final boolean nonblockingApproval;
private IdentityRecord(@NonNull RecipientId recipientId, private IdentityRecord(@NonNull RecipientId recipientId,
IdentityKey identitykey, VerifiedStatus verifiedStatus, IdentityKey identitykey,
boolean firstUse, long timestamp, boolean nonblockingApproval) VerifiedStatus verifiedStatus,
boolean firstUse,
long timestamp,
boolean nonblockingApproval)
{ {
this.recipientId = recipientId; this.recipientId = recipientId;
this.identitykey = identitykey; this.identitykey = identitykey;
@ -293,7 +365,7 @@ public class IdentityDatabase extends Database {
} }
public class IdentityReader { public static class IdentityReader {
private final Cursor cursor; private final Cursor cursor;
IdentityReader(@NonNull Cursor cursor) { IdentityReader(@NonNull Cursor cursor) {

Wyświetl plik

@ -802,7 +802,7 @@ public class RecipientDatabase extends Database {
try { try {
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0); 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) { } catch (InvalidKeyException e) {
Log.w(TAG, "Failed to process identity key during insert! Skipping.", 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()) { if (update.getNew().getIdentityKey().isPresent()) {
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0); 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); 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) { private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); 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 " + 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; + " LEFT OUTER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
List<RecipientSettings> out = new ArrayList<>(); List<RecipientSettings> out = new ArrayList<>();
@ -2913,7 +2913,7 @@ public class RecipientDatabase extends Database {
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid)); db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid));
// Identities // Identities
db.delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.RECIPIENT_ID + " = ?", SqlUtil.buildArgs(byE164)); db.delete(IdentityDatabase.TABLE_NAME, IdentityDatabase.ADDRESS + " = ?", SqlUtil.buildArgs(byE164));
// Group Receipts // Group Receipts
ContentValues groupReceiptValues = new ContentValues(); ContentValues groupReceiptValues = new ContentValues();

Wyświetl plik

@ -211,8 +211,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int AVATAR_PICKER = 111; private static final int AVATAR_PICKER = 111;
private static final int THREAD_CLEANUP = 112; private static final int THREAD_CLEANUP = 112;
private static final int SESSION_MIGRATION = 113; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; 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"); 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

Wyświetl plik

@ -14,6 +14,8 @@ import java.util.concurrent.TimeUnit;
public final class IdentityRecordList { public final class IdentityRecordList {
public static final IdentityRecordList EMPTY = new IdentityRecordList(Collections.emptyList());
private final List<IdentityRecord> identityRecords; private final List<IdentityRecord> identityRecords;
private final boolean isVerified; private final boolean isVerified;
private final boolean isUnverified; private final boolean isUnverified;

Wyświetl plik

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