/* * 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 . */ package org.tm.archive.database import android.content.Context import androidx.core.content.contentValuesOf import org.greenrobot.eventbus.EventBus import org.signal.core.util.Base64 import org.signal.core.util.delete import org.signal.core.util.exists import org.signal.core.util.firstOrNull import org.signal.core.util.logging.Log import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt import org.signal.core.util.requireLong import org.signal.core.util.requireNonNullString import org.signal.core.util.select import org.signal.core.util.toOptional import org.signal.core.util.update import org.signal.libsignal.protocol.IdentityKey import org.tm.archive.database.SignalDatabase.Companion.recipients import org.tm.archive.database.model.IdentityRecord import org.tm.archive.database.model.IdentityStoreRecord import org.tm.archive.dependencies.ApplicationDependencies import org.tm.archive.recipients.Recipient import org.tm.archive.recipients.RecipientId import org.tm.archive.storage.StorageSyncHelper import org.tm.archive.util.IdentityUtil import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.util.UuidUtil import java.lang.AssertionError import java.util.Optional class IdentityTable internal constructor(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper) { companion object { private val TAG = Log.tag(IdentityTable::class.java) const val TABLE_NAME = "identities" private const val ID = "_id" const val ADDRESS = "address" const val IDENTITY_KEY = "identity_key" private const val FIRST_USE = "first_use" private const val TIMESTAMP = "timestamp" const val VERIFIED = "verified" private const val NONBLOCKING_APPROVAL = "nonblocking_approval" const val 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 ) """ } fun getIdentityStoreRecord(serviceId: ServiceId?): IdentityStoreRecord? { return if (serviceId != null) { getIdentityStoreRecord(serviceId.toString()) } else { null } } fun getIdentityStoreRecord(addressName: String): IdentityStoreRecord? { readableDatabase .select() .from(TABLE_NAME) .where("$ADDRESS = ?", addressName) .run() .use { cursor -> if (cursor.moveToFirst()) { return IdentityStoreRecord( addressName = addressName, identityKey = IdentityKey(Base64.decode(cursor.requireNonNullString(IDENTITY_KEY)), 0), verifiedStatus = VerifiedStatus.forState(cursor.requireInt(VERIFIED)), firstUse = cursor.requireBoolean(FIRST_USE), timestamp = cursor.requireLong(TIMESTAMP), nonblockingApproval = cursor.requireBoolean(NONBLOCKING_APPROVAL) ) } else if (UuidUtil.isUuid(addressName)) { val byServiceId = recipients.getByServiceId(ServiceId.parseOrThrow(addressName)) if (byServiceId.isPresent) { val 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.") } } return null } fun saveIdentity( addressName: String, recipientId: RecipientId, identityKey: IdentityKey, verifiedStatus: VerifiedStatus, firstUse: Boolean, timestamp: Long, nonBlockingApproval: Boolean ) { saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval) recipients.markNeedsSync(recipientId) StorageSyncHelper.scheduleSyncForDataChange() } fun setApproval(addressName: String, recipientId: RecipientId, nonBlockingApproval: Boolean) { val updated = writableDatabase .update(TABLE_NAME) .values(NONBLOCKING_APPROVAL to nonBlockingApproval) .where("$ADDRESS = ?", addressName) .run() if (updated > 0) { recipients.markNeedsSync(recipientId) StorageSyncHelper.scheduleSyncForDataChange() } } fun setVerified(addressName: String, recipientId: RecipientId, identityKey: IdentityKey, verifiedStatus: VerifiedStatus) { val updated = writableDatabase .update(TABLE_NAME) .values(VERIFIED to verifiedStatus.toInt()) .where("$ADDRESS = ? AND $IDENTITY_KEY = ?", addressName, Base64.encodeWithPadding(identityKey.serialize())) .run() if (updated > 0) { val record = getIdentityRecord(addressName) if (record.isPresent) { EventBus.getDefault().post(record.get()) } recipients.markNeedsSync(recipientId) StorageSyncHelper.scheduleSyncForDataChange() } } fun updateIdentityAfterSync(addressName: String, recipientId: RecipientId, identityKey: IdentityKey, verifiedStatus: VerifiedStatus) { val existingRecord = getIdentityRecord(addressName) val hadEntry = existingRecord.isPresent val keyMatches = hasMatchingKey(addressName, identityKey) val statusMatches = keyMatches && hasMatchingStatus(addressName, identityKey, verifiedStatus) if (!keyMatches || !statusMatches) { saveIdentityInternal(addressName, recipientId, identityKey, verifiedStatus, !hadEntry, System.currentTimeMillis(), nonBlockingApproval = true) val 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().identityKey.hashCode() + ", New: " + identityKey.hashCode()) IdentityUtil.markIdentityUpdate(context, recipientId) } } fun delete(addressName: String) { writableDatabase .delete(TABLE_NAME) .where("$ADDRESS = ?", addressName) .run() } private fun getIdentityRecord(addressName: String): Optional { return readableDatabase .select() .from(TABLE_NAME) .where("$ADDRESS = ?", addressName) .run() .firstOrNull { cursor -> IdentityRecord( recipientId = RecipientId.fromSidOrE164(cursor.requireNonNullString(ADDRESS)), identityKey = IdentityKey(Base64.decode(cursor.requireNonNullString(IDENTITY_KEY)), 0), verifiedStatus = VerifiedStatus.forState(cursor.requireInt(VERIFIED)), firstUse = cursor.requireBoolean(FIRST_USE), timestamp = cursor.requireLong(TIMESTAMP), nonblockingApproval = cursor.requireBoolean(NONBLOCKING_APPROVAL) ) } .toOptional() } private fun hasMatchingKey(addressName: String, identityKey: IdentityKey): Boolean { return readableDatabase .exists(TABLE_NAME) .where("$ADDRESS = ? AND $IDENTITY_KEY = ?", addressName, Base64.encodeWithPadding(identityKey.serialize())) .run() } private fun hasMatchingStatus(addressName: String, identityKey: IdentityKey, verifiedStatus: VerifiedStatus): Boolean { return readableDatabase .exists(TABLE_NAME) .where("$ADDRESS = ? AND $IDENTITY_KEY = ? AND $VERIFIED = ?", addressName, Base64.encodeWithPadding(identityKey.serialize()), verifiedStatus.toInt()) .run() } private fun saveIdentityInternal( addressName: String, recipientId: RecipientId, identityKey: IdentityKey, verifiedStatus: VerifiedStatus, firstUse: Boolean, timestamp: Long, nonBlockingApproval: Boolean ) { val contentValues = contentValuesOf( ADDRESS to addressName, IDENTITY_KEY to Base64.encodeWithPadding(identityKey.serialize()), TIMESTAMP to timestamp, VERIFIED to verifiedStatus.toInt(), NONBLOCKING_APPROVAL to if (nonBlockingApproval) 1 else 0, FIRST_USE to if (firstUse) 1 else 0 ) writableDatabase.replace(TABLE_NAME, null, contentValues) EventBus.getDefault().post(IdentityRecord(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval)) } enum class VerifiedStatus { DEFAULT, VERIFIED, UNVERIFIED; fun toInt(): Int { return when (this) { DEFAULT -> 0 VERIFIED -> 1 UNVERIFIED -> 2 } } companion object { @JvmStatic fun forState(state: Int): VerifiedStatus { return when (state) { 0 -> DEFAULT 1 -> VERIFIED 2 -> UNVERIFIED else -> throw AssertionError("No such state: $state") } } } } }