session-ios/SessionMessagingKit/Database/Models/BlindedIdLookup.swift

175 lines
6.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
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 hasnt 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,
sessionId: String? = nil,
openGroupServer: String,
openGroupPublicKey: String,
isCheckingForOutbox: Bool,
dependencies: SMKDependencies = SMKDependencies()
) 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 }
// 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
}
// 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
let contactsThatApprovedMeCursor: RecordCursor<Contact> = try Contact
.filter(Contact.Columns.didApproveMe == true)
.fetchCursor(db)
while let contact: Contact = try contactsThatApprovedMeCursor.next() {
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)
// 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)
.updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true))
}
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
// 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?)
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)
}
}