// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import SessionUtil import SessionUtilitiesKit // MARK: - Size Restrictions public extension LibSession { static var sizeAuthDataBytes: Int { 100 } static var sizeSubaccountBytes: Int { 36 } static var sizeSubaccountSigBytes: Int { 64 } static var sizeSubaccountSignatureBytes: Int { 64 } } // MARK: - Group Keys Handling internal extension LibSession { /// `libSession` manages keys entirely so there is no need for a DB presence static let columnsRelatedToGroupKeys: [ColumnExpression] = [] } // MARK: - Incoming Changes internal extension LibSessionCacheType { func handleGroupKeysUpdate( _ db: Database, in config: LibSession.Config?, groupSessionId: SessionId ) throws { guard case .groupKeys(let conf, let infoConf, let membersConf) = config else { throw LibSessionError.invalidConfigObject } /// If the group had been flagged as "expired" (because it got no config messages when initially polling) then receiving a config /// message means the group is no longer expired, so update it's state let groupFlaggedAsExpired: Bool = (try? ClosedGroup .filter(id: groupSessionId.hexString) .select(.expired) .asRequest(of: Bool.self) .fetchOne(db)) .defaulting(to: false) if groupFlaggedAsExpired { try ClosedGroup .filter(id: groupSessionId.hexString) .updateAllAndConfig( db, ClosedGroup.Columns.expired.set(to: false), using: dependencies ) } /// If two admins rekeyed for different member changes at the same time then there is a "key collision" and the "needs rekey" function /// will return true to indicate that a 3rd `rekey` needs to be made to have a final set of keys which includes all members /// /// **Note:** We don't check `needsDump` in this case because the local state _could_ be persisted yet still require a `rekey` /// so we should rely solely on `groups_keys_needs_rekey` guard groups_keys_needs_rekey(conf) else { return } // Performing a `rekey` returns the updated key data which we don't use directly, this updated // key will now be returned by `groups_keys_pending_config` which the `ConfigurationSyncJob` uses // when generating pending changes for group keys so we don't need to push it directly var pushResult: UnsafePointer? = nil var pushResultLen: Int = 0 guard groups_keys_rekey(conf, infoConf, membersConf, &pushResult, &pushResultLen) else { throw LibSessionError.failedToRekeyGroup } } } // MARK: - Outgoing Changes internal extension LibSession { static func rekey( _ db: Database, groupSessionId: SessionId, using dependencies: Dependencies ) throws { try dependencies.mutate(cache: .libSession) { cache in try cache.performAndPushChange(db, for: .groupKeys, sessionId: groupSessionId) { config in guard case .groupKeys(let conf, let infoConf, let membersConf) = config else { throw LibSessionError.invalidConfigObject } // Performing a `rekey` returns the updated key data which we don't use directly, this updated // key will now be returned by `groups_keys_pending_config` which the `ConfigurationSyncJob` uses // when generating pending changes for group keys so we don't need to push it directly var pushResult: UnsafePointer? = nil var pushResultLen: Int = 0 guard groups_keys_rekey(conf, infoConf, membersConf, &pushResult, &pushResultLen) else { throw LibSessionError.failedToRekeyGroup } } } } static func keySupplement( _ db: Database, groupSessionId: SessionId, memberIds: Set, using dependencies: Dependencies ) throws -> Data { return try dependencies.mutate(cache: .libSession) { cache in guard case .groupKeys(let conf, _, _) = cache.config(for: .groupKeys, sessionId: groupSessionId) else { throw LibSessionError.invalidConfigObject } var cMemberIds: [UnsafePointer?] = ( try? (memberIds .map { id in id.cString(using: .utf8) } .unsafeCopyCStringArray())) .defaulting(to: []) defer { cMemberIds.forEach { $0?.deallocate() } } // Performing a `key_supplement` returns the supplemental key changes, since our state doesn't care // about the `GROUP_KEYS` needed for other members this change won't result in the `GROUP_KEYS` config // going into a pending state or the `ConfigurationSyncJob` getting triggered so return the data so that // the caller can push it directly var cSupplementData: UnsafeMutablePointer! var cSupplementDataLen: Int = 0 guard groups_keys_key_supplement(conf, &cMemberIds, cMemberIds.count, &cSupplementData, &cSupplementDataLen), let cSupplementData: UnsafeMutablePointer = cSupplementData else { throw LibSessionError.failedToKeySupplementGroup } // Must deallocate on success let supplementData: Data = Data( bytes: cSupplementData, count: cSupplementDataLen ) cSupplementData.deallocate() return supplementData } } static func loadAdminKey( _ db: Database, groupIdentitySeed: Data, groupSessionId: SessionId, using dependencies: Dependencies ) throws { try dependencies.mutate(cache: .libSession) { cache in /// Disable the admin check because we are about to convert the user to being an admin and it's guaranteed to fail try cache.withCustomBehaviour(.skipGroupAdminCheck, for: groupSessionId) { try cache.performAndPushChange(db, for: .groupKeys, sessionId: groupSessionId) { config in guard case .groupKeys(let conf, let infoConf, let membersConf) = config else { throw LibSessionError.invalidConfigObject } var identitySeed: [UInt8] = Array(groupIdentitySeed) groups_keys_load_admin_key(conf, &identitySeed, infoConf, membersConf) try LibSessionError.throwIfNeeded(conf) } } } } static func numKeys( groupSessionId: SessionId, using dependencies: Dependencies ) throws -> Int { return try dependencies.mutate(cache: .libSession) { cache in guard case .groupKeys(let conf, _, _) = cache.config(for: .groupKeys, sessionId: groupSessionId) else { throw LibSessionError.invalidConfigObject } return Int(groups_keys_size(conf)) } } static func currentGeneration( groupSessionId: SessionId, using dependencies: Dependencies ) throws -> Int { return try dependencies.mutate(cache: .libSession) { cache in guard case .groupKeys(let conf, _, _) = cache.config(for: .groupKeys, sessionId: groupSessionId) else { throw LibSessionError.invalidConfigObject } return Int(groups_keys_current_generation(conf)) } } }