2022-06-08 06:29:51 +02:00
|
|
|
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
|
|
|
|
|
import Foundation
|
2022-06-09 10:37:44 +02:00
|
|
|
|
import GRDB
|
|
|
|
|
import SessionUtilitiesKit
|
|
|
|
|
|
|
|
|
|
/// This lookup is created when the user interacts with a blinded id
|
|
|
|
|
public struct BlindedIdLookup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
|
|
|
|
public static var databaseTableName: String { "blindedIdLookup" }
|
|
|
|
|
|
|
|
|
|
public typealias Columns = CodingKeys
|
|
|
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
|
|
|
case blindedId
|
|
|
|
|
case sessionId
|
|
|
|
|
case openGroupServer
|
|
|
|
|
case openGroupPublicKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var id: String { blindedId }
|
|
|
|
|
|
|
|
|
|
/// The blinded id for the user on this open group server
|
|
|
|
|
public let blindedId: String
|
|
|
|
|
|
|
|
|
|
/// The standard sessionId which can be used to generate this blindedId on this open group server
|
|
|
|
|
///
|
|
|
|
|
/// **Note:** This value will be null if the user owning the blinded id hasn’t accepted the message request
|
|
|
|
|
public let sessionId: String?
|
|
|
|
|
|
|
|
|
|
/// The server for the Open Group server this blinded id belongs to
|
|
|
|
|
public let openGroupServer: String
|
|
|
|
|
|
|
|
|
|
/// The public key for the Open Group server this blinded id belongs to
|
|
|
|
|
public let openGroupPublicKey: String
|
|
|
|
|
|
|
|
|
|
// MARK: - Initialization
|
|
|
|
|
|
|
|
|
|
public init(
|
|
|
|
|
blindedId: String,
|
|
|
|
|
sessionId: String? = nil,
|
|
|
|
|
openGroupServer: String,
|
|
|
|
|
openGroupPublicKey: String
|
|
|
|
|
) {
|
|
|
|
|
self.blindedId = blindedId
|
|
|
|
|
self.sessionId = sessionId
|
|
|
|
|
self.openGroupServer = openGroupServer
|
|
|
|
|
self.openGroupPublicKey = openGroupPublicKey
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Mutation
|
|
|
|
|
|
|
|
|
|
public extension BlindedIdLookup {
|
|
|
|
|
func with(sessionId: String) -> BlindedIdLookup {
|
|
|
|
|
return BlindedIdLookup(
|
|
|
|
|
blindedId: self.blindedId,
|
|
|
|
|
sessionId: sessionId,
|
|
|
|
|
openGroupServer: self.openGroupServer,
|
|
|
|
|
openGroupPublicKey: self.openGroupPublicKey
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - GRDB Interactions
|
|
|
|
|
|
|
|
|
|
public extension BlindedIdLookup {
|
|
|
|
|
/// Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard sessionId, as a result in order
|
|
|
|
|
/// to see if there is an unblinded contact for this blindedId we can only really generate blinded ids for each contact and check
|
|
|
|
|
/// if any match
|
|
|
|
|
///
|
|
|
|
|
/// If we can't find a match this method will still store a lookup, just with no standard sessionId value (this gives us a method to
|
|
|
|
|
/// link back to the open group the blindedId originated from)
|
|
|
|
|
static func fetchOrCreate(
|
|
|
|
|
_ db: Database,
|
|
|
|
|
blindedId: String,
|
2022-07-29 07:26:24 +02:00
|
|
|
|
sessionId: String? = nil,
|
2022-06-09 10:37:44 +02:00
|
|
|
|
openGroupServer: String,
|
|
|
|
|
openGroupPublicKey: String,
|
2022-06-29 10:10:10 +02:00
|
|
|
|
isCheckingForOutbox: Bool,
|
2022-06-22 06:27:34 +02:00
|
|
|
|
dependencies: SMKDependencies = SMKDependencies()
|
2022-06-09 10:37:44 +02:00
|
|
|
|
) throws -> BlindedIdLookup {
|
|
|
|
|
var lookup: BlindedIdLookup = (try? BlindedIdLookup
|
|
|
|
|
.fetchOne(db, id: blindedId))
|
|
|
|
|
.defaulting(
|
|
|
|
|
to: BlindedIdLookup(
|
|
|
|
|
blindedId: blindedId,
|
|
|
|
|
openGroupServer: openGroupServer.lowercased(),
|
|
|
|
|
openGroupPublicKey: openGroupPublicKey
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// If the lookup already has a resolved sessionId then just return it immediately
|
|
|
|
|
guard lookup.sessionId == nil else { return lookup }
|
|
|
|
|
|
2022-07-29 07:26:24 +02:00
|
|
|
|
// If we we given a sessionId then validate it is correct and if so save it
|
|
|
|
|
if
|
|
|
|
|
let sessionId: String = sessionId,
|
|
|
|
|
dependencies.sodium.sessionId(
|
|
|
|
|
sessionId,
|
|
|
|
|
matchesBlindedId: blindedId,
|
|
|
|
|
serverPublicKey: openGroupPublicKey,
|
|
|
|
|
genericHash: dependencies.genericHash
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
lookup = try lookup
|
|
|
|
|
.with(sessionId: sessionId)
|
|
|
|
|
.saved(db)
|
|
|
|
|
return lookup
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-09 10:37:44 +02:00
|
|
|
|
// We now need to try to match the blinded id to an existing contact, this can only be done by looping
|
|
|
|
|
// through all approved contacts and generating a blinded id for the provided open group for each to
|
|
|
|
|
// see if it matches the provided blindedId
|
2022-06-29 10:10:10 +02:00
|
|
|
|
let contactsThatApprovedMeCursor: RecordCursor<Contact> = try Contact
|
|
|
|
|
.filter(Contact.Columns.didApproveMe == true)
|
2022-06-09 10:37:44 +02:00
|
|
|
|
.fetchCursor(db)
|
2022-06-29 10:10:10 +02:00
|
|
|
|
|
|
|
|
|
while let contact: Contact = try contactsThatApprovedMeCursor.next() {
|
2022-06-09 10:37:44 +02:00
|
|
|
|
guard dependencies.sodium.sessionId(contact.id, matchesBlindedId: blindedId, serverPublicKey: openGroupPublicKey, genericHash: dependencies.genericHash) else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We found a match so update the lookup and leave the loop
|
|
|
|
|
lookup = try lookup
|
|
|
|
|
.with(sessionId: contact.id)
|
|
|
|
|
.saved(db)
|
2022-06-29 10:10:10 +02:00
|
|
|
|
|
|
|
|
|
// There is an edge-case where the contact might not have their 'isApproved' flag set to true
|
|
|
|
|
// but if we have a `BlindedIdLookup` for them and are performing the lookup from the outbox
|
|
|
|
|
// then that means we sent them a message request and the 'isApproved' flag should be true
|
|
|
|
|
if isCheckingForOutbox && !contact.isApproved {
|
|
|
|
|
try Contact
|
|
|
|
|
.filter(id: contact.id)
|
|
|
|
|
.updateAll(db, Contact.Columns.isApproved.set(to: true))
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-09 10:37:44 +02:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finish if we have a result
|
|
|
|
|
guard lookup.sessionId == nil else { return lookup }
|
|
|
|
|
|
|
|
|
|
// Lastly loop through existing id lookups (in case the user is looking at a different SOGS but once had
|
2022-06-16 05:14:56 +02:00
|
|
|
|
// a thread with this contact in a different SOGS and had cached the lookup) - we really should never hit
|
|
|
|
|
// this case since the contact approval status is sync'ed (the only situation I can think of is a config
|
|
|
|
|
// message hasn't been handled correctly?)
|
2022-06-09 10:37:44 +02:00
|
|
|
|
let blindedIdLookupCursor: RecordCursor<BlindedIdLookup> = try BlindedIdLookup
|
|
|
|
|
.filter(BlindedIdLookup.Columns.sessionId != nil)
|
|
|
|
|
.filter(BlindedIdLookup.Columns.openGroupServer != openGroupServer.lowercased())
|
|
|
|
|
.fetchCursor(db)
|
|
|
|
|
|
|
|
|
|
while let otherLookup: BlindedIdLookup = try blindedIdLookupCursor.next() {
|
|
|
|
|
guard
|
|
|
|
|
let sessionId: String = otherLookup.sessionId,
|
|
|
|
|
dependencies.sodium.sessionId(
|
|
|
|
|
sessionId,
|
|
|
|
|
matchesBlindedId: blindedId,
|
|
|
|
|
serverPublicKey: openGroupPublicKey,
|
|
|
|
|
genericHash: dependencies.genericHash
|
|
|
|
|
)
|
|
|
|
|
else { continue }
|
|
|
|
|
|
|
|
|
|
// We found a match so update the lookup and leave the loop
|
|
|
|
|
lookup = try lookup
|
|
|
|
|
.with(sessionId: sessionId)
|
|
|
|
|
.saved(db)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Want to save the lookup even if it doesn't have a sessionId so it can be used when handling
|
|
|
|
|
// MessageRequestResponse messages
|
|
|
|
|
return try lookup
|
|
|
|
|
.saved(db)
|
|
|
|
|
}
|
|
|
|
|
}
|