kopia lustrzana https://github.com/ryukoposting/Signal-Android
283 wiersze
13 KiB
Java
283 wiersze
13 KiB
Java
/*
|
|
* Copyright (C) 2011 Whisper Systems
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms.database;
|
|
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import org.greenrobot.eventbus.EventBus;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.signal.libsignal.protocol.IdentityKey;
|
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
|
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
|
import org.thoughtcrime.securesms.database.model.IdentityStoreRecord;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
|
import org.thoughtcrime.securesms.util.Base64;
|
|
import org.signal.core.util.CursorUtil;
|
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
|
import org.signal.core.util.SqlUtil;
|
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Optional;
|
|
|
|
public class IdentityTable extends DatabaseTable {
|
|
|
|
@SuppressWarnings("unused")
|
|
private static final String TAG = Log.tag(IdentityTable.class);
|
|
|
|
static final String TABLE_NAME = "identities";
|
|
private static final String ID = "_id";
|
|
static final String ADDRESS = "address";
|
|
static final String IDENTITY_KEY = "identity_key";
|
|
private static final String FIRST_USE = "first_use";
|
|
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 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() {
|
|
switch (this) {
|
|
case DEFAULT: return 0;
|
|
case VERIFIED: return 1;
|
|
case UNVERIFIED: return 2;
|
|
default: throw new AssertionError();
|
|
}
|
|
}
|
|
|
|
public static VerifiedStatus forState(int state) {
|
|
switch (state) {
|
|
case 0: return DEFAULT;
|
|
case 1: return VERIFIED;
|
|
case 2: return UNVERIFIED;
|
|
default: throw new AssertionError("No such state: " + state);
|
|
}
|
|
}
|
|
}
|
|
|
|
IdentityTable(Context context, SignalDatabase databaseHelper) {
|
|
super(context, databaseHelper);
|
|
}
|
|
|
|
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);
|
|
} else if (UuidUtil.isUuid(addressName)) {
|
|
Optional<RecipientId> byServiceId = SignalDatabase.recipients().getByServiceId(ServiceId.parseOrThrow(addressName));
|
|
|
|
if (byServiceId.isPresent()) {
|
|
Recipient recipient = Recipient.resolved(byServiceId.get());
|
|
|
|
if (recipient.hasE164() && !UuidUtil.isUuid(recipient.requireE164())) {
|
|
Log.i(TAG, "Could not find identity for UUID. Attempting E164.");
|
|
return getIdentityStoreRecord(recipient.requireE164());
|
|
} else {
|
|
Log.i(TAG, "Could not find identity for UUID, and our recipient doesn't have an E164.");
|
|
}
|
|
} else {
|
|
Log.i(TAG, "Could not find identity for UUID, and we don't have a recipient.");
|
|
}
|
|
} else {
|
|
Log.i(TAG, "Could not find identity for E164 either.");
|
|
}
|
|
} catch (InvalidKeyException | IOException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void saveIdentity(@NonNull String addressName,
|
|
@NonNull RecipientId recipientId,
|
|
IdentityKey identityKey,
|
|
VerifiedStatus verifiedStatus,
|
|
boolean firstUse,
|
|
long timestamp,
|
|
boolean nonBlockingApproval)
|
|
{
|
|
saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
|
|
SignalDatabase.recipients().markNeedsSync(recipientId);
|
|
StorageSyncHelper.scheduleSyncForDataChange();
|
|
}
|
|
|
|
public void setApproval(@NonNull String addressName, @NonNull RecipientId recipientId, boolean nonBlockingApproval) {
|
|
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
|
|
|
ContentValues contentValues = new ContentValues(2);
|
|
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval);
|
|
|
|
database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", SqlUtil.buildArgs(addressName));
|
|
|
|
SignalDatabase.recipients().markNeedsSync(recipientId);
|
|
StorageSyncHelper.scheduleSyncForDataChange();
|
|
}
|
|
|
|
public void setVerified(@NonNull String addressName, @NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
|
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
|
|
|
String query = ADDRESS + " = ? AND " + IDENTITY_KEY + " = ?";
|
|
String[] args = SqlUtil.buildArgs(addressName, Base64.encodeBytes(identityKey.serialize()));
|
|
|
|
ContentValues contentValues = new ContentValues(1);
|
|
contentValues.put(VERIFIED, verifiedStatus.toInt());
|
|
|
|
int updated = database.update(TABLE_NAME, contentValues, query, args);
|
|
|
|
if (updated > 0) {
|
|
Optional<IdentityRecord> record = getIdentityRecord(addressName);
|
|
if (record.isPresent()) EventBus.getDefault().post(record.get());
|
|
SignalDatabase.recipients().markNeedsSync(recipientId);
|
|
StorageSyncHelper.scheduleSyncForDataChange();
|
|
}
|
|
}
|
|
|
|
public void updateIdentityAfterSync(@NonNull String addressName, @NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
|
Optional<IdentityRecord> existingRecord = getIdentityRecord(addressName);
|
|
|
|
boolean hadEntry = existingRecord.isPresent();
|
|
boolean keyMatches = hasMatchingKey(addressName, identityKey);
|
|
boolean statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus);
|
|
|
|
if (!keyMatches || !statusMatches) {
|
|
saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), true);
|
|
|
|
Optional<IdentityRecord> record = getIdentityRecord(addressName);
|
|
|
|
if (record.isPresent()) {
|
|
EventBus.getDefault().post(record.get());
|
|
}
|
|
|
|
ApplicationDependencies.getProtocolStore().aci().identities().invalidate(addressName);
|
|
}
|
|
|
|
if (hadEntry && !keyMatches) {
|
|
Log.w(TAG, "Updated identity key during storage sync for " + addressName + " | Existing: " + existingRecord.get().getIdentityKey().hashCode() + ", New: " + identityKey.hashCode());
|
|
IdentityUtil.markIdentityUpdate(context, recipientId);
|
|
}
|
|
}
|
|
|
|
public void delete(@NonNull String addressName) {
|
|
databaseHelper.getSignalWritableDatabase().delete(IdentityTable.TABLE_NAME, IdentityTable.ADDRESS + " = ?", SqlUtil.buildArgs(addressName));
|
|
}
|
|
|
|
private Optional<IdentityRecord> getIdentityRecord(@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()) {
|
|
return Optional.of(getIdentityRecord(cursor));
|
|
}
|
|
} catch (InvalidKeyException | IOException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
|
|
return Optional.empty();
|
|
}
|
|
|
|
private boolean hasMatchingKey(@NonNull String addressName, IdentityKey identityKey) {
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
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 String addressName, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
|
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
|
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();
|
|
}
|
|
}
|
|
|
|
private static @NonNull IdentityRecord getIdentityRecord(@NonNull Cursor cursor) throws IOException, InvalidKeyException {
|
|
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.fromSidOrE164(addressName), identity, VerifiedStatus.forState(verifiedStatus), firstUse, timestamp, nonblockingApproval);
|
|
}
|
|
|
|
private void saveIdentityInternal(@NonNull String addressName,
|
|
@NonNull RecipientId recipientId,
|
|
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(ADDRESS, addressName);
|
|
contentValues.put(IDENTITY_KEY, identityKeyString);
|
|
contentValues.put(TIMESTAMP, timestamp);
|
|
contentValues.put(VERIFIED, verifiedStatus.toInt());
|
|
contentValues.put(NONBLOCKING_APPROVAL, nonBlockingApproval ? 1 : 0);
|
|
contentValues.put(FIRST_USE, firstUse ? 1 : 0);
|
|
|
|
database.replace(TABLE_NAME, null, contentValues);
|
|
|
|
EventBus.getDefault().post(new IdentityRecord(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval));
|
|
}
|
|
}
|