diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java index c3258783b7..5ea98d21cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java @@ -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 = identityDatabase.getIdentity(address.getName()); RecipientId recipientId = RecipientId.fromExternalPush(address.getName()); - Optional 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)) { - return identityKey.equals(IdentityKeyUtil.getIdentityKey(context)); - } + if (isSelf) { + return identityKey.equals(IdentityKeyUtil.getIdentityKey(context)); + } - 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); - } + IdentityStoreRecord record = DatabaseFactory.getIdentityDatabase(context).getIdentityStoreRecord(address.getName()); + + switch (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 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) { - 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java index f2b1f250d5..9b22702b99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java @@ -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,37 +50,40 @@ 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, " + - IDENTITY_KEY + " TEXT, " + - FIRST_USE + " INTEGER DEFAULT 0, " + - TIMESTAMP + " INTEGER DEFAULT 0, " + - VERIFIED + " INTEGER DEFAULT 0, " + - NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);"; + 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, " + + VERIFIED + " INTEGER DEFAULT 0, " + + NONBLOCKING_APPROVAL + " INTEGER DEFAULT 0);"; public enum VerifiedStatus { 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 getIdentity(@NonNull RecipientId recipientId) { + public Optional 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 Optional 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 recipients) { - List records = new LinkedList<>(); - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String[] selectionArgs = new String[1]; + List addressNames = recipients.stream() + .filter(Recipient::hasServiceIdentifier) + .map(Recipient::requireServiceId) + .collect(Collectors.toList()); - database.beginTransaction(); - try { - for (Recipient recipient : recipients) { - selectionArgs[0] = recipient.getId().serialize(); + if (addressNames.isEmpty()) { + return IdentityRecordList.EMPTY; + } - try (Cursor cursor = database.query(TABLE_NAME, null, RECIPIENT_ID + " = ?", selectionArgs, null, null, null)) { - if (cursor.moveToFirst()) { - records.add(getIdentityRecord(cursor)); - } + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + SqlUtil.Query query = SqlUtil.buildCollectionQuery(ADDRESS, addressNames); + + List 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 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 record = getIdentity(id); - if (record.isPresent()) EventBus.getDefault().post(record.get()); + saveIdentityInternal(addressName, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true); + + Optional 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 50d4d5163e..ae2c8d6f04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -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 newIdentityRecord = identityDatabase.getIdentity(recipientId); @@ -1093,7 +1093,7 @@ public class RecipientDatabase extends Database { private List 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 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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index c8a1e98fc9..f517dfd859 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java b/app/src/main/java/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java index d588fa413f..4da39bbf5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/identity/IdentityRecordList.java @@ -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 identityRecords; private final boolean isVerified; private final boolean isUnverified; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/IdentityStoreRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/IdentityStoreRecord.kt new file mode 100644 index 0000000000..dd5c5395ca --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/IdentityStoreRecord.kt @@ -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 +)