diff --git a/signalc/migrations/changelog.postgresql.sql b/signalc/migrations/changelog.postgresql.sql index ceaff1c0c56677e672cfa28bf78bb717c23e5227..acb2dade9a645245a922b3cbaf224be2af6f4d96 100644 --- a/signalc/migrations/changelog.postgresql.sql +++ b/signalc/migrations/changelog.postgresql.sql @@ -167,4 +167,51 @@ CREATE TABLE IF NOT EXISTS contacts ( ); CREATE INDEX contacts_account_id_uuid ON contacts (account_id, uuid); CREATE INDEX contacts_account_id_phone_number ON contacts (account_id, phone_number); --- rollback DROP TABLE contacts; \ No newline at end of file +-- rollback DROP TABLE contacts; + + +-- changeset aguestuser:1623100049239-1 failOnError:true +DELETE FROM accounts; +DELETE FROM contacts; +DELETE FROM identities; +DELETE FROM ownidentities; +DELETE FROM prekeys; +DELETE FROM profiles; +DELETE FROM senderkeys; +DELETE FROM sessions; +DELETE FROM signedprekeys; + +ALTER TABLE identities DROP CONSTRAINT pk_identities; +ALTER TABLE identities DROP COLUMN contact_id; +ALTER TABLE identities ADD COLUMN contact_id INT; +ALTER TABLE identities ADD CONSTRAINT pk_identities PRIMARY KEY (account_id, contact_id); + +ALTER TABLE sessions DROP CONSTRAINT pk_sessions; +ALTER TABLE sessions DROP COLUMN contact_id; +ALTER TABLE sessions ADD COLUMN contact_id INT; +ALTER TABLE sessions ADD CONSTRAINT pk_sessions PRIMARY KEY (account_id, contact_id, device_id); +-- rollback DELETE FROM accounts; +-- rollback DELETE FROM contacts; +-- rollback DELETE FROM identities; +-- rollback DELETE FROM ownidentities; +-- rollback DELETE FROM prekeys; +-- rollback DELETE FROM profiles; +-- rollback DELETE FROM senderkeys; +-- rollback DELETE FROM sessions; +-- rollback DELETE FROM signedprekeys; +-- rollback +-- rollback ALTER TABLE identities DROP CONSTRAINT pk_identities; +-- rollback ALTER TABLE identities DROP COLUMN contact_id; +-- rollback ALTER TABLE identities ADD COLUMN contact_id VARCHAR(255) NOT NULL; +-- rollback ALTER TABLE identities ADD CONSTRAINT pk_identities PRIMARY KEY (account_id, contact_id); +-- rollback +-- rollback ALTER TABLE sessions DROP CONSTRAINT pk_sessions; +-- rollback ALTER TABLE sessions DROP COLUMN contact_id; +-- rollback ALTER TABLE sessions ADD COLUMN contact_id VARCHAR(255) NOT NULL; +-- rollback ALTER TABLE sessions ADD CONSTRAINT pk_sessions PRIMARY KEY (account_id, contact_id, device_id); + +-- changeset aguestuser:1623100049239-2 failOnError:true +DROP INDEX identities_identity_key_bytes; +-- rollback CREATE INDEX identities_identity_key_bytes ON identities (identity_key_bytes); + +-- TODO: uniqueness constraint on contacts account_id_uuid \ No newline at end of file diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt index 042d4c36b5ba2a7f32d274e7ed8d66ed9a1d3a04..3f93ce0c4bad5f07dfeb43294ecfca1621bc0a48 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/Application.kt @@ -7,7 +7,7 @@ import info.signalboost.signalc.logging.LibSignalLogger import info.signalboost.signalc.logic.* import info.signalboost.signalc.metrics.Metrics import info.signalboost.signalc.store.AccountStore -import info.signalboost.signalc.store.ProfileStore +import info.signalboost.signalc.store.ContactStore import info.signalboost.signalc.store.ProtocolStore import io.mockk.coEvery import io.mockk.mockk @@ -152,7 +152,7 @@ class Application(val config: Config.App){ // STORE // lateinit var accountStore: AccountStore - lateinit var profileStore: ProfileStore + lateinit var contactStore: ContactStore lateinit var protocolStore: ProtocolStore private lateinit var dataSource: HikariDataSource @@ -240,7 +240,7 @@ class Application(val config: Config.App){ // storage resources dataSource = initializeDataSource(Mocks.dataSource) accountStore = initializeColdComponent(AccountStore::class) - profileStore = initializeColdComponent(ProfileStore::class, Mocks.profileStore) + contactStore = initializeColdComponent(ContactStore::class, Mocks.contactStore) protocolStore = initializeColdComponent(ProtocolStore::class, Mocks.protocolStore) // network resources diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt index 5424c5442125759d71f8a328c8e2c67c93420f17..57dbb78765bba41877d8d0c597e5f0bbf7e80164 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/Config.kt @@ -4,7 +4,7 @@ import com.zaxxer.hikari.HikariDataSource import info.signalboost.signalc.logic.* import info.signalboost.signalc.metrics.Metrics import info.signalboost.signalc.store.AccountStore -import info.signalboost.signalc.store.ProfileStore +import info.signalboost.signalc.store.ContactStore import info.signalboost.signalc.store.ProtocolStore import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi @@ -26,7 +26,7 @@ object Config { HikariDataSource::class, AccountStore::class, ProtocolStore::class, - ProfileStore::class, + ContactStore::class, // components AccountManager::class, SignalReceiver::class, diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt b/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt index 177c7126a96bd5be1517d6ee6b01987b7b330751..b2d793c68c119c56bbc66c36bfaec5ed7c323788 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/Mocks.kt @@ -5,7 +5,7 @@ import info.signalboost.signalc.logic.* import info.signalboost.signalc.metrics.Metrics import info.signalboost.signalc.model.SignalcAddress import info.signalboost.signalc.model.SignalcSendResult -import info.signalboost.signalc.store.ProfileStore +import info.signalboost.signalc.store.ContactStore import info.signalboost.signalc.store.ProtocolStore import io.mockk.coEvery import io.mockk.every @@ -34,7 +34,7 @@ object Mocks { val dataSource: HikariDataSource.() -> Unit = { every { closeQuietly() } returns Unit } - val profileStore: ProfileStore.() -> Unit = { + val contactStore: ContactStore.() -> Unit = { coEvery { storeProfileKey(any(), any(), any())} returns Unit coEvery { loadProfileKey(any(), any())} returns mockk() } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt index bfa50aca3978fc8bacc8374f6db0391a9851f949..0d3e5fa173ddc482aa4948b9879b1935dbfe801a 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/ContactRecord.kt @@ -10,18 +10,18 @@ import org.jetbrains.exposed.sql.statements.UpdateStatement interface ContactRecord: FieldSet { val accountId: Column<String> - val contactId: Column<String> + val contactId: Column<Int> companion object { - fun ContactRecord.findByContactId(accountId: String, contactId: String): ResultRow? { + fun ContactRecord.findByContactId(accountId: String, contactId: Int): ResultRow? { val table = this return table.select { (table.accountId eq accountId).and(table.contactId eq contactId) }.singleOrNull() } - fun ContactRecord.findManyByContactId(accountId: String, contactId: String): List<ResultRow> { + fun ContactRecord.findManyByContactId(accountId: String, contactId: Int): List<ResultRow> { val table = this return table.select { (table.accountId eq accountId).and(table.contactId eq contactId) @@ -30,7 +30,7 @@ interface ContactRecord: FieldSet { fun ContactRecord.updateByContactId( accountId: String, - contactId: String, + contactId: Int, updateStatement: Table.(UpdateStatement) -> Unit ): Int { val table = this @@ -39,7 +39,7 @@ interface ContactRecord: FieldSet { }, null, updateStatement) } - fun ContactRecord.deleteByContactId(accountId: String, contactId: String): Int { + fun ContactRecord.deleteByContactId(accountId: String, contactId: Int): Int { val table = this return (table as Table).deleteWhere { (table.accountId eq accountId).and(table.contactId eq contactId) diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt index 6a6a87dee15fd14c5f87f5852396e22293669d0d..0853924f97273ecfaebd18ea280fac108357a849 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Contacts.kt @@ -2,9 +2,9 @@ package info.signalboost.signalc.db import org.jetbrains.exposed.sql.Table -object Contacts: Table() { - val accountId = varchar("account_id", 255) - val contactId = integer("contact_id").autoIncrement() +object Contacts: Table(), ContactRecord { + override val accountId = varchar("account_id", 255) + override val contactId = integer("contact_id").autoIncrement() val uuid = uuid("uuid").nullable() val phoneNumber = varchar("phone_number", 255) val profileKeyBytes = binary("profile_key_bytes").nullable() diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt index 24aa4f9bc40005bdd806c3b160b94da4898104f0..669cfe0affcc681a48ac8b86515ae5e9e35963b7 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/DeviceRecord.kt @@ -2,43 +2,43 @@ package info.signalboost.signalc.db import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.UpdateStatement -import org.whispersystems.libsignal.SignalProtocolAddress interface DeviceRecord: FieldSet { val accountId: Column<String> - val contactId: Column<String> + val contactId: Column<Int> val deviceId: Column<Int> companion object { - fun DeviceRecord.findByAddress(accountId: String, address: SignalProtocolAddress): ResultRow? { + fun DeviceRecord.findByDeviceId(accountId: String, contactId: Int, deviceId: Int): ResultRow? { val table = this return table.select { (table.accountId eq accountId) - .and(table.contactId eq address.name) - .and(table.deviceId eq address.deviceId) + .and(table.contactId eq contactId) + .and(table.deviceId eq deviceId) }.singleOrNull() } - fun DeviceRecord.updateByAddress( + fun DeviceRecord.updateByDeviceId( accountId: String, - address: SignalProtocolAddress, + contactId: Int, + deviceId: Int, updateStatement: Table.(UpdateStatement) -> Unit ): Int { val table = this return (table as Table).update ({ (table.accountId eq accountId) - .and(table.contactId eq address.name) - .and(table.deviceId eq address.deviceId) + .and(table.contactId eq contactId) + .and(table.deviceId eq deviceId) }, null, updateStatement) } - fun DeviceRecord.deleteByAddress(accountId: String, address: SignalProtocolAddress): Int { + fun DeviceRecord.deleteByDeviceId(accountId: String, contactId: Int, deviceId: Int): Int { val table = this return (table as Table).deleteWhere { (table.accountId eq accountId) - .and(table.contactId eq address.name) - .and(table.deviceId eq address.deviceId) + .and(table.contactId eq contactId) + .and(table.deviceId eq deviceId) } } } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt index 4295ac292be2437fb68ef9e658e1273df6f50223..ac3b608d7a21018606ec8b4f2499698806a3a947 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Identities.kt @@ -9,7 +9,7 @@ object Identities: Table(), ContactRecord { private const val IDENTITY_KEY_BYTE_ARRAY_LENGTH = 33 override val accountId = varchar("account_id", 255) - override val contactId = varchar("contact_id", 255) + override val contactId = integer("contact_id") val identityKeyBytes = binary("identity_key_bytes", IDENTITY_KEY_BYTE_ARRAY_LENGTH).index() val isTrusted = bool("is_trusted").default(true) val createdAt = timestamp("created_at").clientDefault { Instant.now() } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt b/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt index 4ca084f585dd39c2806690a2ed5e459a869c8b59..b0d859c9fd8968362365f145922ddf4a3ac98b23 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/db/Sessions.kt @@ -3,10 +3,9 @@ package info.signalboost.signalc.db import org.jetbrains.exposed.sql.Table - object Sessions: Table(), ContactRecord, DeviceRecord { override val accountId = varchar("account_id", 255) - override val contactId = varchar("contact_id", 255) + override val contactId = integer("contact_id") override val deviceId = integer("device_id") val sessionBytes = binary("session_bytes") diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt b/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt index 5e911d9847867c13304809af7372270b0a1f76de..1ade8209d588c15ca80c5646b276833e4a589bbc 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/exception/SignalcError.kt @@ -8,9 +8,10 @@ object SignalcError { object RegistrationOfRegsisteredUser: Exception("Cannot register account that is already registered") object SubscriptionOfUnregisteredUser: Exception("Cannot subscribe to messages for unregistered account") class UpdateToNonExistentFingerprint( - contactId: String, + accountId: String, + contactId: Int, fingerprint: ByteArray, - ): Exception("Cannot update non-existent fingerprint ${fingerprint.toHex()} for contact $contactId") + ): Exception("Cannot update non-existent fingerprint ${fingerprint.toHex()} for contact $contactId of account $accountId") object UnsubscribeUnregisteredUser: Exception("Cannot unsubscribe to messages for unregistered account") object VerificationOfNewUser: Exception("Cannot verify a new (unregistered) account") object VerificationOfVerifiedUser: Exception("Cannot verify account that is already verified") diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt index ef6c6bfeb9bd984868e18790635997acb5d839ac..56f3ea8de3d3576fe5ef90aeb77589f85d15f640 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/AccountManager.kt @@ -156,7 +156,7 @@ class AccountManager(private val app: Application) { **/ @Throws(IOException::class) // if network call to retreive sender cert fails suspend fun getUnidentifiedAccessPair(accountId: String, contactId: String): UnidentifiedAccessPair? { - val contactAccessKey = app.profileStore.loadProfileKey(accountId, contactId)?.let { + val contactAccessKey = app.contactStore.loadProfileKey(accountId, contactId)?.let { UnidentifiedAccess.deriveAccessKeyFrom(it) } ?: run { logger.error { "Could not derive delivery token for $contactId: no profile key found." } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt index ecb25ba04f197e20d9abbf493b0694cf6137594e..3232b79565f7549a6f2960b9ab50ae9a060ab4cd 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalReceiver.kt @@ -22,12 +22,12 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver import org.whispersystems.signalservice.api.crypto.SignalServiceCipher import org.whispersystems.signalservice.api.messages.SignalServiceAttachment import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope import org.whispersystems.signalservice.api.util.UptimeSleepTimer import java.io.File import java.io.IOException import java.nio.file.Files +import java.util.* import java.util.concurrent.CancellationException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit @@ -168,41 +168,43 @@ class SignalReceiver(private val app: Application) { logger.debug { "Got ${it.asString} from ${envelope.sourceIdentifier ?: "SEALED"} to ${account.username}" } Metrics.SignalReceiver.numberOfMessagesReceived.labels(it.asString).inc() return when (it) { + EnvelopeType.CIPHERTEXT, + EnvelopeType.UNIDENTIFIED_SENDER -> processMessage(envelope, account) + EnvelopeType.PREKEY_BUNDLE -> { - relay(envelope, account) - maybeRefreshPreKeys(account) + processPreKeyBundle(envelope, account) + processMessage(envelope, account) } - EnvelopeType.UNIDENTIFIED_SENDER, - EnvelopeType.CIPHERTEXT -> relay(envelope, account) + EnvelopeType.RECEIPT -> processReceipt(envelope, account) EnvelopeType.KEY_EXCHANGE, // TODO: handle this to process "reset secure session" events - EnvelopeType.RECEIPT, // signal android basically drops these, so do we! EnvelopeType.UNKNOWN -> drop(envelope, account) } - } } - private suspend fun relay(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job { + private suspend fun processMessage(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job { // Attempt to decrypt envelope in a new coroutine then relay result to socket message sender for handling. - val (sender, recipient) = Pair(envelope.asSignalcAddress(), account.address) return app.coroutineScope.launch(Concurrency.Dispatcher) { + var contactAddress: SignalcAddress? = null // not available until after decryption for sealed-sender msgs try { messagesInFlight.getAndIncrement() - val dataMessage: SignalServiceDataMessage = cipherOf(account).decrypt(envelope).dataMessage.orNull() + val contents = cipherOf(account).decrypt(envelope) + val dataMessage = contents.dataMessage.orNull() ?: return@launch // drop other message types (eg: typing message, sync message, etc) + contactAddress = contents.sender.asSignalcAddress() val body = dataMessage.body?.orNull() ?: "" val attachments = dataMessage.attachments.orNull() ?: emptyList() dataMessage.profileKey.orNull()?.let { - app.profileStore.storeProfileKey(recipient.identifier, sender.identifier, it) + app.contactStore.storeProfileKey(account.id, contactAddress.identifier, it) } app.socketSender.send( SocketResponse.Cleartext.of( - sender, - recipient, + contactAddress, + account.address, body, // we allow empty message bodies b/c that's how expiry timer changes are sent attachments.mapNotNull { it.retrieveFor(account) }, dataMessage.expiresInSeconds, @@ -210,20 +212,44 @@ class SignalReceiver(private val app: Application) { ) ) } catch(err: Throwable) { - handleRelayError(err, sender, recipient) + // TODO: we don't always have a contactAddress here anymore... wat do? + handleRelayError(err, account.address, contactAddress) } finally { messagesInFlight.getAndDecrement() } } } - private fun maybeRefreshPreKeys(account: VerifiedAccount) = app.coroutineScope.launch(Concurrency.Dispatcher) { - // If we are receiving a prekey bundle, this is the beginning of a new session, the initiation - // of which might have depleted our prekey reserves below the level we want to keep on hand - // to start new sessions. So: launch a background job to check our prekey reserve and replenish it if needed! - app.accountManager.refreshPreKeysIfDepleted(account) + private suspend fun processReceipt(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? { + if(!envelope.isUnidentifiedSender) { + // NOTE: we should only receive N of these upon initiating a conversation with a new contact... + // (where N is the number of devices a recipient has) + // - q: should this also be able to store a phone number if we only have a UUID? + app.contactStore.storeUuid( + accountId = account.username, + contactPhoneNumber = envelope.sourceE164.get(), + contactUuid = UUID.fromString(envelope.sourceUuid.get()), + ) + } + return null } + + // TODO: make this function suspend and async/await!! + private fun processPreKeyBundle(envelope: SignalServiceEnvelope, account: VerifiedAccount) = + // here: app.coroutineScope.async(...) + app.coroutineScope.launch(Concurrency.Dispatcher) { + app.contactStore.create( + accountId = account.username, + phoneNumber = envelope.sourceE164.get(), + uuid = UUID.fromString(envelope.sourceUuid.get()), + ) + // If we are receiving a prekey bundle, this is the beginning of a new session, the initiation + // of which might have depleted our prekey reserves below the level we want to keep on hand + // to start new sessions. So: launch a background job to check our prekey reserve and replenish it if needed! + app.accountManager.refreshPreKeysIfDepleted(account) + } + private suspend fun drop(envelope: SignalServiceEnvelope, account: VerifiedAccount): Job? { app.socketSender.send( SocketResponse.Dropped(envelope.asSignalcAddress(), account.address, envelope) @@ -231,7 +257,7 @@ class SignalReceiver(private val app: Application) { return null } - private suspend fun handleRelayError(err: Throwable, sender: SignalcAddress, recipient: SignalcAddress) { + private suspend fun handleRelayError(err: Throwable, account: SignalcAddress, contact: SignalcAddress?) { when (err) { is ProtocolUntrustedIdentityException -> { // When we get this exception we return a null fingerprint to the client, with the intention of causing @@ -240,14 +266,14 @@ class SignalReceiver(private val app: Application) { // and can be handled (and trusted) from the send path. app.socketSender.send( SocketResponse.InboundIdentityFailure.of( - recipient, - sender, + account, + contact, null, ) ) } else -> { - app.socketSender.send(SocketResponse.DecryptionError(sender, recipient, err)) + app.socketSender.send(SocketResponse.DecryptionError(account, contact, err)) logger.error { "Decryption Error:\n ${err.stackTraceToString()}" } } } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt index e1e2a962d2ca14511c4d3d8a7628fb4b1a178418..f42e66ca5ec04d8b70e3a7326297dac3ece74d81 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/logic/SignalSender.kt @@ -153,8 +153,8 @@ class SignalSender(private val app: Application) { // Try to send all messages sealed-sender by deriving `unidentifiedAccessPair`s from profile keys. Without such // tokens, message are sent unsealed, and are treated by Signal as spam. To avoid being blocked by Signal, // we expose a toggle to prevent all unsealed messages from being sent. - val unidentifiedAccessPair = app.accountManager.getUnidentifiedAccessPair(sender.identifier,recipient.identifier).also { - metrics.numberOfUnsealedMessagesProduced.labels(sender.identifier).inc() + val unidentifiedAccessPair = app.accountManager.getUnidentifiedAccessPair(sender.id, recipient.identifier).also { + metrics.numberOfUnsealedMessagesProduced.labels(sender.id).inc() if(it == null && app.toggles.blockUnsealedMessages) return@async SignalcSendResult.Blocked(recipient) } try { diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt b/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt index 645b0a31637ee1b6ba607df18e7dd211fed5e10f..cb71f6bfce948836b7242682ed542bb9e282b83f 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/model/Account.kt @@ -196,6 +196,11 @@ data class VerifiedAccount( } } + // accessor that returns what signalboost (currently) uses: a phone number + val id: String + get() = username + + // accessor that returns what signalservice address would return: a uuid val identifier: String get() = uuid.toString() diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt b/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt index 339ae0cad8ad91b3b725cf70862e2b7c84cf2ccf..abf383d903d5c1b28f39e2c12235e059186324a9 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/model/SignalcAddress.kt @@ -34,6 +34,10 @@ data class SignalcAddress( } } } + + val id: String + get() = number!! + val identifier: String get() = uuid?.toString() ?: number!! diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt b/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt index 1d8f8033cb43486b20df74e13e34e5f804e43e79..9cbe2866a28a17e48cc52a6337ae39f9ee94dbe0 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/model/SocketResponse.kt @@ -112,8 +112,9 @@ sealed class SocketResponse { @Serializable @SerialName("decryption_error") data class DecryptionError( - val sender: SignalcAddress, val recipient: SignalcAddress, + @Required + val sender: SignalcAddress?, @Serializable(ThrowableSerializer::class) val error: Throwable, ): SocketResponse() @@ -130,7 +131,8 @@ sealed class SocketResponse { @Serializable data class Data( val local_address: LocalAddress, - val remote_address: RemoteAddress, + @Required + val remote_address: RemoteAddress?, @Required val fingerprint: String? ) @@ -144,8 +146,14 @@ sealed class SocketResponse { data class RemoteAddress(val number: String) companion object { - fun of(localAddress: SignalcAddress, remoteAddress: SignalcAddress, fingerprint: String? = null) = - InboundIdentityFailure(Data(LocalAddress(localAddress.number!!), RemoteAddress(remoteAddress.number!!), fingerprint)) + fun of(localAddress: SignalcAddress, remoteAddress: SignalcAddress?, fingerprint: String? = null) = + InboundIdentityFailure( + Data( + LocalAddress(localAddress.number!!), + remoteAddress?.let{ RemoteAddress(it.identifier) }, + fingerprint + ) + ) } } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..3ee67148f7c7c209b378f013f7192f660bdba92c --- /dev/null +++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ContactStore.kt @@ -0,0 +1,136 @@ +package info.signalboost.signalc.store + +import info.signalboost.signalc.Application +import info.signalboost.signalc.db.ContactRecord.Companion.findByContactId +import info.signalboost.signalc.db.ContactRecord.Companion.updateByContactId +import info.signalboost.signalc.db.Contacts +import info.signalboost.signalc.db.Profiles +import info.signalboost.signalc.dispatchers.Concurrency +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.runBlocking +import mu.KLoggable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.transactions.transaction +import org.signal.zkgroup.profiles.ProfileKey +import java.util.UUID +import kotlin.io.path.ExperimentalPathApi +import kotlin.time.ExperimentalTime + +@ExperimentalCoroutinesApi +@ObsoleteCoroutinesApi +@ExperimentalPathApi +@ExperimentalTime +class ContactStore( + app: Application, + // TODO: need to consume session lock for a given account? +) { + + companion object: Any(), KLoggable { + override val logger = logger() + private const val VALID_PROFILE_KEY_SIZE = 32 + } + + val dispatcher = Concurrency.Dispatcher + val db = app.db + + suspend fun create( + accountId: String, + phoneNumber: String? = null, + uuid: UUID? = null, + profileKey: ByteArray? = null + ): Int = + newSuspendedTransaction(dispatcher, db) { + Contacts.insert { + it[Contacts.accountId] = accountId + it[Contacts.phoneNumber] = phoneNumber + it[Contacts.uuid] = uuid + it[profileKeyBytes] = profileKey + } + }.resultedValues!!.single()[Contacts.contactId] + + suspend fun resolveContactIdSuspend(accountId: String, identifier: String): Int = + newSuspendedTransaction (dispatcher, db) { + resolveContactId(accountId, identifier) + } + + fun resolveContactIdBlocking(accountId: String, identifier: String): Int = + transaction(db) { + resolveContactId(accountId, identifier) + } + + private fun resolveContactId(accountId: String, identifier: String): Int { + val uuid = parseUuid(identifier) + return run { + uuid + ?.let { + logger.debug { "Found uuid: $uuid" } + Contacts.select { (Contacts.accountId eq accountId).and(Contacts.uuid eq it) } + } + ?: Contacts.select { + logger.debug { "Found phone number: $identifier" } + (Contacts.accountId eq accountId).and(Contacts.phoneNumber eq identifier) + } + }.single().let { it[Contacts.contactId] } +// }.singleOrNull() +// ?.let { +// it[Contacts.contactId] +// } +// ?: runBlocking { +// logger.debug { "Creating new contact w/ identifier: $uuid" } +// val phoneNumber: String? = if(uuid == null) identifier else null +// create(accountId, phoneNumber, uuid) +// } + } + + private fun parseUuid(identifier: String): UUID? = + try { + UUID.fromString(identifier) + } catch(ignored: Throwable) { + null + } + + suspend fun storeUuid(accountId: String, contactPhoneNumber: String, contactUuid: UUID) { + // TODO: this currently only adds a uuid to a phonenumber-based contact record + // it shoudl be able to add either a uuid or aphonenumber + newSuspendedTransaction(dispatcher, db) { + val contactId = resolveContactId(accountId, contactPhoneNumber) + Contacts.updateByContactId(accountId, contactId) { + it[Contacts.uuid] = contactUuid + } + } + } + + suspend fun storeProfileKey(accountId: String, contactIdentifier: String, profileKey: ByteArray) { + if (profileKey.size != VALID_PROFILE_KEY_SIZE) { + logger.warn { "Received profile key of invalid size ${profileKey.size} for account: $contactIdentifier" } + return + } + val contactId = resolveContactIdSuspend(accountId, contactIdentifier) + return newSuspendedTransaction(dispatcher, db) { + Contacts.updateByContactId(accountId, contactId) { + it[Contacts.profileKeyBytes] = profileKey + } + } + } + + suspend fun loadProfileKey(accountId: String, contactIdentifier: String): ProfileKey? = + newSuspendedTransaction(dispatcher, db) { + val contactId = resolveContactIdSuspend(accountId, contactIdentifier) + Contacts.findByContactId(accountId, contactId)?.let { + ProfileKey(it[Contacts.profileKeyBytes]) + } + } + + // TESTING FUNCTIONS + internal suspend fun count(): Long = newSuspendedTransaction(dispatcher, db) { + Profiles.selectAll().count() + } + + internal suspend fun deleteAllFor(accountId: String) = newSuspendedTransaction(dispatcher, db) { + Profiles.deleteWhere { + Profiles.accountId eq accountId + } + } +} \ No newline at end of file diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProfileStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProfileStore.kt deleted file mode 100644 index b45a451501b4a8691aa9119b5f43bd158ffd2a2c..0000000000000000000000000000000000000000 --- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProfileStore.kt +++ /dev/null @@ -1,67 +0,0 @@ -package info.signalboost.signalc.store - -import info.signalboost.signalc.Application -import info.signalboost.signalc.db.Profiles -import info.signalboost.signalc.dispatchers.Concurrency -import info.signalboost.signalc.serialization.ByteArrayEncoding.toPostgresHex -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.ObsoleteCoroutinesApi -import mu.KLoggable -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction -import org.signal.zkgroup.profiles.ProfileKey -import kotlin.io.path.ExperimentalPathApi -import kotlin.time.ExperimentalTime - -@ExperimentalCoroutinesApi -@ObsoleteCoroutinesApi -@ExperimentalPathApi -@ExperimentalTime -class ProfileStore(app: Application) { - companion object: Any(), KLoggable { - override val logger = logger() - private const val VALID_PROFILE_KEY_SIZE = 32 - } - - val dispatcher = Concurrency.Dispatcher - val db = app.db - - suspend fun storeProfileKey(accountId: String, contactId: String, profileKey: ByteArray): Unit { - if (profileKey.size != VALID_PROFILE_KEY_SIZE) { - logger.warn { "Received profile key of invalid size ${profileKey.size} for account: $accountId" } - return - } - return newSuspendedTransaction(dispatcher, db) { - exec( - "INSERT into profiles (account_id, contact_id, profile_key_bytes) " + - "VALUES('$accountId','$contactId', ${profileKey.toPostgresHex()}) " + - "ON CONFLICT (account_id, contact_id)" + - "DO UPDATE set profile_key_bytes = EXCLUDED.profile_key_bytes;" - ) - } - } - - suspend fun loadProfileKey(accountId: String, contactId: String): ProfileKey? = - newSuspendedTransaction(dispatcher, db) { - Profiles.select { - (Profiles.accountId eq accountId).and( - Profiles.contactId eq contactId - ) - }.singleOrNull()?.let { - ProfileKey(it[Profiles.profileKeyBytes]) - } - } - - // TESTING FUNCTIONS - internal suspend fun count(): Long = newSuspendedTransaction(dispatcher, db) { - Profiles.selectAll().count() - } - - internal suspend fun deleteAllFor(accountId: String) = newSuspendedTransaction(dispatcher, db) { - Profiles.deleteWhere { - Profiles.accountId eq accountId - } - } - - -} \ No newline at end of file diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt index 4e0573e8bd4dc77a119a1963bc2b8096299556fa..516830e81267bce7376694e84b88f73b298ed38f 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/ProtocolStore.kt @@ -14,7 +14,6 @@ import org.whispersystems.libsignal.groups.state.SenderKeyStore import org.whispersystems.libsignal.state.* import org.whispersystems.signalservice.api.SignalServiceProtocolStore import org.whispersystems.signalservice.api.SignalServiceSessionStore -import org.whispersystems.signalservice.api.push.SignalServiceAddress import java.time.Instant import kotlin.io.path.ExperimentalPathApi import kotlin.time.ExperimentalTime @@ -24,9 +23,15 @@ import kotlin.time.ExperimentalTime @ObsoleteCoroutinesApi @ExperimentalPathApi @ExperimentalTime -class ProtocolStore(app: Application) { +class ProtocolStore(val app: Application) { val db = app.db - fun of(account: Account): AccountProtocolStore = AccountProtocolStore(db, account.username) + fun of(account: Account): AccountProtocolStore = AccountProtocolStore( + db, + account.username, + // TODO: if we need to resolve race in signal sender for first incoming message, + // we might need to construct this in the constructor such that it consumes a session lock (for the account)... + app.contactStore::resolveContactIdBlocking + ) fun countOwnIdentities(): Long = transaction(db) { OwnIdentities.selectAll().count() } @@ -34,11 +39,13 @@ class ProtocolStore(app: Application) { class AccountProtocolStore( private val db: Database, private val accountId: String, + private val resolveContactId: (String, String) -> Int, val lock: SessionLock = SessionLock(), - private val identityStore: IdentityKeyStore = SignalcIdentityStore(db, accountId, lock), + // if we need to do above, we ahve to construct ContactStore + private val identityStore: IdentityKeyStore = SignalcIdentityStore(db, accountId, lock, resolveContactId), private val preKeyStore: PreKeyStore = SignalcPreKeyStore(db, accountId, lock), private val senderKeyStore: SenderKeyStore = SignalcSenderKeyStore(db, accountId, lock), - private val sessionStore: SignalServiceSessionStore = SignalcSessionStore(db, accountId, lock), + private val sessionStore: SignalServiceSessionStore = SignalcSessionStore(db, accountId, lock, resolveContactId), private val signedPreKeyStore: SignedPreKeyStore = SignalcSignedPreKeyStore(db, accountId, lock), ) : SignalServiceProtocolStore, IdentityKeyStore by identityStore, diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt index 19f55a2e54f679af2207843f61a496db96e47c47..6827e049e81c64afe80d44c94aea9abe5132610b 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcIdentityStore.kt @@ -25,6 +25,7 @@ class SignalcIdentityStore( val db: Database, val accountId: String, val lock: SessionLock, + val resolveContactId: (String, String) -> Int, ): IdentityKeyStore{ override fun getIdentityKeyPair(): IdentityKeyPair = @@ -57,8 +58,9 @@ class SignalcIdentityStore( @Throws(SignalcError.UpdateToNonExistentFingerprint::class) fun trustFingerprint(address: SignalProtocolAddress, fingerprint: ByteArray) { lock.acquireForTransaction(db) { - rejectUnknownFingerprint(address, fingerprint) - Identities.updateByContactId(accountId, address.name) { + val contactId = resolveContactId(accountId, address.name) + rejectUnknownFingerprint(contactId, fingerprint) + Identities.updateByContactId(accountId, contactId) { it[isTrusted] = true it[updatedAt] = Instant.now() } @@ -68,22 +70,22 @@ class SignalcIdentityStore( @Throws(SignalcError.UpdateToNonExistentFingerprint::class) fun untrustFingerprint(address: SignalProtocolAddress, fingerprint: ByteArray) { lock.acquireForTransaction(db) { - rejectUnknownFingerprint(address, fingerprint) - Identities.updateByContactId(accountId, address.name) { + val contactId = resolveContactId(accountId, address.name) + rejectUnknownFingerprint(contactId, fingerprint) + Identities.updateByContactId(accountId, contactId) { it[isTrusted] = false it[updatedAt] = Instant.now() } } } - private fun rejectUnknownFingerprint(address: SignalProtocolAddress, fingerprint: ByteArray) { + private fun rejectUnknownFingerprint(contactId: Int, fingerprint: ByteArray) { transaction(db) { - val contactId = address.name val knownFingerprint = Identities.findByContactId(accountId, contactId)?.let { it[identityKeyBytes] } if (!fingerprint.contentEquals(knownFingerprint)) - throw SignalcError.UpdateToNonExistentFingerprint(contactId, fingerprint) + throw SignalcError.UpdateToNonExistentFingerprint(accountId, contactId, fingerprint) } } @@ -93,7 +95,7 @@ class SignalcIdentityStore( // - deny trust for subsequent identity keys for same address // Returns true if this save was an update to an existing record, false otherwise lock.acquireForTransaction(db) { - val contactId = address.name + val contactId = resolveContactId(accountId, address.name) Identities.findByContactId(accountId, contactId) ?.let { existingKey -> Identities.updateByContactId(accountId, contactId) { @@ -121,7 +123,7 @@ class SignalcIdentityStore( ): Boolean = lock.acquireForTransaction(db) { // trust a key if... - Identities.findByContactId(accountId, address.name)?.let { + Identities.findByContactId(accountId, resolveContactId(accountId, address.name))?.let { // it matches a key we have seen before it[identityKeyBytes] contentEquals identityKey.serialize() && // and we have not flagged it as untrusted @@ -131,14 +133,14 @@ class SignalcIdentityStore( override fun getIdentity(address: SignalProtocolAddress): IdentityKey? = lock.acquireForTransaction(db) { - Identities.findByContactId(accountId, address.name)?.let { + Identities.findByContactId(accountId, resolveContactId(accountId, address.name))?.let { IdentityKey(it[identityKeyBytes], 0) } } fun removeIdentity(address: SignalProtocolAddress) { lock.acquireForTransaction(db) { - Identities.deleteByContactId(accountId, address.name) + Identities.deleteByContactId(accountId, resolveContactId(accountId, address.name)) } } @@ -158,8 +160,7 @@ class SignalcIdentityStore( fun whenIdentityLastUpdated(address: SignalProtocolAddress): Instant? = transaction(db) { - val contactId = address.name - Identities.findByContactId(accountId, contactId)?.let { + Identities.findByContactId(accountId, resolveContactId(accountId, address.name))?.let { it[updatedAt] } } diff --git a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt index d09a1d2ad695c49030f09de889c7d5ac57c837ed..4423a5fd3b3684a49ed88aeb1267ca07868d6b59 100644 --- a/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt +++ b/signalc/src/main/kotlin/info/signalboost/signalc/store/protocol/SignalcSessionStore.kt @@ -1,10 +1,11 @@ package info.signalboost.signalc.store.protocol import info.signalboost.signalc.db.* +import info.signalboost.signalc.db.ContactRecord.Companion.deleteByContactId import info.signalboost.signalc.db.ContactRecord.Companion.findManyByContactId -import info.signalboost.signalc.db.DeviceRecord.Companion.deleteByAddress -import info.signalboost.signalc.db.DeviceRecord.Companion.findByAddress -import info.signalboost.signalc.db.DeviceRecord.Companion.updateByAddress +import info.signalboost.signalc.db.DeviceRecord.Companion.deleteByDeviceId +import info.signalboost.signalc.db.DeviceRecord.Companion.findByDeviceId +import info.signalboost.signalc.db.DeviceRecord.Companion.updateByDeviceId import info.signalboost.signalc.db.Sessions.sessionBytes import org.jetbrains.exposed.sql.* import org.whispersystems.libsignal.SignalProtocolAddress @@ -16,11 +17,13 @@ class SignalcSessionStore( val db: Database, val accountId: String, val lock: SessionLock, + val resolveContactId: (String, String) -> Int, ): SignalServiceSessionStore { override fun loadSession(address: SignalProtocolAddress): SessionRecord = lock.acquireForTransaction(db) { - Sessions.findByAddress(accountId, address)?.let { + val contactId = resolveContactId(accountId, address.name) + Sessions.findByDeviceId(accountId, contactId, address.deviceId)?.let { SessionRecord(it[sessionBytes]) } ?: SessionRecord() } @@ -28,34 +31,35 @@ class SignalcSessionStore( override fun getSubDeviceSessions(name: String): MutableList<Int> = lock.acquireForTransaction(db) { Sessions.select { - Sessions.accountId eq accountId and (Sessions.contactId eq name) + (Sessions.accountId eq accountId).and(Sessions.contactId eq resolveContactId(accountId, name)) }.mapTo(mutableListOf()) { it[Sessions.deviceId] } } override fun storeSession(address: SignalProtocolAddress, record: SessionRecord) { // upsert the session record for a given address lock.acquireForTransaction(db) { - Sessions.updateByAddress(accountId, address) { - it[sessionBytes] = record.serialize() - }.let { numUpdated -> - if (numUpdated == 0) { - Sessions.insert { - it[accountId] = this@SignalcSessionStore.accountId - it[contactId] = address.name - it[deviceId] = address.deviceId - it[sessionBytes] = record.serialize() - } + val contactId = resolveContactId(accountId, address.name) + Sessions.updateByDeviceId(accountId, contactId, address.deviceId) { + it[sessionBytes] = record.serialize() + }.let { numUpdated -> + if (numUpdated == 0) { + Sessions.insert { + it[accountId] = this@SignalcSessionStore.accountId + it[Sessions.contactId] = contactId + it[deviceId] = address.deviceId + it[sessionBytes] = record.serialize() } } } } + } override fun containsSession(address: SignalProtocolAddress): Boolean = lock.acquireForTransaction(db) { - Sessions.findByAddress(accountId, address)?.let { + val contactId = resolveContactId(accountId, address.name) + Sessions.findByDeviceId(accountId, contactId, address.deviceId)?.let { val sessionRecord = SessionRecord(it[sessionBytes]) - sessionRecord.hasSenderChain() - && sessionRecord.sessionVersion == CiphertextMessage.CURRENT_VERSION; + sessionRecord.hasSenderChain() && sessionRecord.sessionVersion == CiphertextMessage.CURRENT_VERSION } ?: false } @@ -63,22 +67,22 @@ class SignalcSessionStore( override fun deleteSession(address: SignalProtocolAddress) { lock.acquireForTransaction(db) { - Sessions.deleteByAddress(accountId, address) + val contactId = resolveContactId(accountId, address.name) + Sessions.deleteByDeviceId(accountId, contactId, address.deviceId) } } override fun deleteAllSessions(name: String) { lock.acquireForTransaction(db) { - Sessions.deleteWhere { - Sessions.accountId eq accountId and (Sessions.contactId eq name) - } + Sessions.deleteByContactId(accountId, resolveContactId(accountId, name)) } } override fun archiveSession(address: SignalProtocolAddress) { lock.acquireForTransaction(db) { - Sessions.findByAddress(accountId, address)?.let { + val contactId = resolveContactId(accountId, address.name) + Sessions.findByDeviceId(accountId, contactId, address.deviceId)?.let { val session = SessionRecord(it[sessionBytes]) session.archiveCurrentState() storeSession(address, session) @@ -88,7 +92,7 @@ class SignalcSessionStore( fun archiveAllSessions(address: SignalProtocolAddress) { lock.acquireForTransaction(db) { - val contactId = address.name + val contactId = resolveContactId(accountId, address.name) Sessions.findManyByContactId(accountId, contactId).forEach { val session = SessionRecord(it[sessionBytes]) session.archiveCurrentState() @@ -96,6 +100,4 @@ class SignalcSessionStore( } } } - - } \ No newline at end of file diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt index a05427a9c071567ba7d0e4d16af2b8bc49e8d6bc..8c352ad7d4b29ae40fb8fac944dc05027c7f997e 100644 --- a/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt +++ b/signalc/src/test/kotlin/info/signalboost/signalc/logic/AccountManagerTest.kt @@ -293,7 +293,7 @@ class AccountManagerTest : FreeSpec({ } returns senderCert.serialized coEvery { - app.profileStore.loadProfileKey(verifiedAccount.identifier, any()) + app.contactStore.loadProfileKey(verifiedAccount.identifier, any()) } coAnswers { if (secondArg<String>() == knownContactId) contactProfileKey else null diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt index 284eed27ef3d37112ba2152824608bf1477b43ec..6ca55166a29b1e1b61004704408612657aad9933 100644 --- a/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt +++ b/signalc/src/test/kotlin/info/signalboost/signalc/logic/SignalReceiverTest.kt @@ -428,7 +428,7 @@ class SignalReceiverTest : FreeSpec({ messageReceiver.subscribe(recipientAccount) eventually(timeout) { coVerify { - app.profileStore.storeProfileKey( + app.contactStore.storeProfileKey( recipientAccount.address.identifier, senderAddress.identifier, fakeProfileKey, diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt index 4cd4882c894ad2babc8a13e35900f6f319d92e91..553391e6f38039b2a6bd365975698e27b6c62036 100644 --- a/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt +++ b/signalc/src/test/kotlin/info/signalboost/signalc/model/SocketResponseTest.kt @@ -181,8 +181,8 @@ class SocketResponseTest : FreeSpec({ |{ |"type":"decryption_error", |"sender":{ - |"number":"${response.sender.number}", - |"uuid":"${response.sender.uuid}" + |"number":"${response.sender?.number}", + |"uuid":"${response.sender?.uuid}" |}, |"recipient":{ |"number":"${response.recipient.number}", @@ -219,7 +219,7 @@ class SocketResponseTest : FreeSpec({ |"number":"${response.data.local_address.number}" |}, |"remote_address":{ - |"number":"${response.data.remote_address.number}" + |"number":"${response.data.remote_address?.number}" |}, |"fingerprint":"${response.data.fingerprint}" |} diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/store/ProfileStoreTest.kt b/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt similarity index 64% rename from signalc/src/test/kotlin/info/signalboost/signalc/store/ProfileStoreTest.kt rename to signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt index 8abba4eebecfdd5c8a8998002d01e762a85eeba3..ea6b6ab750be708866f315e0e9ac79e43f6aeb4d 100644 --- a/signalc/src/test/kotlin/info/signalboost/signalc/store/ProfileStoreTest.kt +++ b/signalc/src/test/kotlin/info/signalboost/signalc/store/ContactStoreTest.kt @@ -1,7 +1,6 @@ package info.signalboost.signalc.store import info.signalboost.signalc.util.KeyUtil.genRandomBytes -import org.signal.zkgroup.profiles.ProfileKey import com.zaxxer.hikari.HikariDataSource import info.signalboost.signalc.Application import info.signalboost.signalc.Config @@ -19,10 +18,10 @@ import kotlin.time.ExperimentalTime @ExperimentalTime @ObsoleteCoroutinesApi @ExperimentalCoroutinesApi -class ProfileStoreTest: FreeSpec({ +class ContactStoreTest: FreeSpec({ runBlockingTest { val testScope = this - val config = Config.mockAllExcept(ProfileStore::class, HikariDataSource::class) + val config = Config.mockAllExcept(ContactStore::class, HikariDataSource::class) val app = Application(config).run(testScope) val accountId = genPhoneNumber() @@ -37,45 +36,45 @@ class ProfileStoreTest: FreeSpec({ "Profile store" - { afterTest { - app.profileStore.deleteAllFor(accountId) + app.contactStore.deleteAllFor(accountId) } "#loadProfileKey" - { beforeTest { - app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey) + app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey) } "returns profile key for a contact if it exists" { - app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey + app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey } "returns null if no profile key exists for contact" { - app.profileStore.loadProfileKey(accountId, genPhoneNumber()) shouldBe null + app.contactStore.loadProfileKey(accountId, genPhoneNumber()) shouldBe null } } "#storeProfileKey" - { "stores a new profile key for a new contact" { - val startingCount = app.profileStore.count() - app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey) - app.profileStore.count() shouldBe startingCount + 1 - app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey + val startingCount = app.contactStore.count() + app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey) + app.contactStore.count() shouldBe startingCount + 1 + app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey } "overwrites the old profile key for an existing contact" { - val startingCount = app.profileStore.count() - app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey) - app.profileStore.storeProfileKey(accountId, contactId, contactUpdatedProfileKey) - app.profileStore.count() shouldBe startingCount + 1 - app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactUpdatedProfileKey + val startingCount = app.contactStore.count() + app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey) + app.contactStore.storeProfileKey(accountId, contactId, contactUpdatedProfileKey) + app.contactStore.count() shouldBe startingCount + 1 + app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactUpdatedProfileKey } "safely stores the same profile key twice" { - val startingCount = app.profileStore.count() - app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey) - app.profileStore.storeProfileKey(accountId, contactId, contactProfileKey) - app.profileStore.count() shouldBe startingCount + 1 - app.profileStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey + val startingCount = app.contactStore.count() + app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey) + app.contactStore.storeProfileKey(accountId, contactId, contactProfileKey) + app.contactStore.count() shouldBe startingCount + 1 + app.contactStore.loadProfileKey(accountId, contactId)?.serialize() shouldBe contactProfileKey } } } diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt index ca0393078f2ac8c6ddd7766fb60f14bf9848193d..6fc0f581b898fcd0a8aae32cd2b0f1f33fe23ace 100644 --- a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt +++ b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/db/MigrationUtil.kt @@ -1,8 +1,6 @@ package info.signalboost.signalc.testSupport.db -import info.signalboost.signalc.db.Identities -import info.signalboost.signalc.db.Profiles -import info.signalboost.signalc.db.SenderKeys +import info.signalboost.signalc.db.Contacts import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction @@ -31,7 +29,7 @@ import org.jetbrains.exposed.sql.transactions.transaction fun main() { // change this assignment to - val table = Profiles + val table = Contacts val db = Database.connect( driver = "org.postgresql.Driver", url = "jdbc:postgresql://localhost:5432/signalc_scratch", diff --git a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt index b0d277fdde334dc41188f0eda189c40ac2143039..32918f4cb3f65ba4d5b6cbfb961948e5c2e6d860 100644 --- a/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt +++ b/signalc/src/test/kotlin/info/signalboost/signalc/testSupport/matchers/SocketResponseMatchers.kt @@ -52,7 +52,7 @@ object SocketResponseMatchers { fingerprint: String? ): SocketResponse.InboundIdentityFailure = match { it.data.local_address.number == recipient.number && - it.data.remote_address.number == sender.number && + it.data.remote_address?.number == sender.number && it.data.fingerprint == fingerprint }