|  |  |  |  | // 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 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, | 
					
						
							|  |  |  |  |         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) | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | } |