You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionMessagingKit/LibSession/Config Handling/LibSession+SharedGroup.swift

368 lines
14 KiB
Swift

// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionSnodeKit
import SessionUtil
import SessionUtilitiesKit
// MARK: - Group Domains
public extension LibSession.Crypto.Domain {
static var kickedMessage: LibSession.Crypto.Domain = "SessionGroupKickedMessage" // stringlint:disable
}
// MARK: - Convenience
internal extension LibSession {
typealias CreatedGroupInfo = (
groupSessionId: SessionId,
identityKeyPair: KeyPair,
groupState: [ConfigDump.Variant: Config],
group: ClosedGroup,
members: [GroupMember]
)
static func createGroup(
_ db: Database,
name: String,
description: String?,
displayPictureUrl: String?,
displayPictureFilename: String?,
displayPictureEncryptionKey: Data?,
members: [(id: String, profile: Profile?)],
using dependencies: Dependencies
) throws -> CreatedGroupInfo {
guard
let groupIdentityKeyPair: KeyPair = dependencies[singleton: .crypto].generate(.ed25519KeyPair()),
let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db)
else { throw MessageSenderError.noKeyPair }
// Prep the relevant details (reduce the members to ensure we don't accidentally insert duplicates)
let groupSessionId: SessionId = SessionId(.group, publicKey: groupIdentityKeyPair.publicKey)
let creationTimestamp: TimeInterval = TimeInterval(dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000)
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let currentUserProfile: Profile? = Profile.fetchOrCreateCurrentUser(db, using: dependencies)
// Create the new config objects
let groupState: [ConfigDump.Variant: Config] = try createGroupState(
groupSessionId: groupSessionId,
userED25519KeyPair: userED25519KeyPair,
groupIdentityPrivateKey: Data(groupIdentityKeyPair.secretKey),
shouldLoadState: false, // We manually load the state after populating the configs
using: dependencies
)
// Extract the conf objects from the state to load in the initial data
guard case .groupKeys(let groupKeysConf, let groupInfoConf, let groupMembersConf) = groupState[.groupKeys] else {
Log.error(.libSession, "Group config objects were null")
throw LibSessionError.unableToCreateConfigObject
}
// Set the initial values in the confs
var cName: [CChar] = try name.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }()
groups_info_set_name(groupInfoConf, &cName)
groups_info_set_created(groupInfoConf, Int64(floor(creationTimestamp)))
if let groupDescription: String = description {
var cDesc: [CChar] = try groupDescription.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }()
groups_info_set_description(groupInfoConf, &cDesc)
}
if
let displayPictureUrl: String = displayPictureUrl,
let displayPictureEncryptionKey: Data = displayPictureEncryptionKey
{
var displayPic: user_profile_pic = user_profile_pic()
displayPic.set(\.url, to: displayPictureUrl)
displayPic.set(\.key, to: displayPictureEncryptionKey)
groups_info_set_pic(groupInfoConf, displayPic)
}
// Throw if there was an error setting up the group info
try LibSessionError.throwIfNeeded(groupInfoConf)
// Load in the initial admin & members
struct MemberInfo: Hashable {
let id: String
let isAdmin: Bool
let profile: Profile?
}
try members
.filter { $0.id != userSessionId.hexString }
.map { id, profile in MemberInfo(id: id, isAdmin: false, profile: profile) }
.appending(MemberInfo(id: userSessionId.hexString, isAdmin: true, profile: currentUserProfile))
.asSet()
.forEach { memberInfo in
var member: config_group_member = config_group_member()
member.set(\.session_id, to: memberInfo.id)
member.set(\.name, to: (memberInfo.profile?.name ?? ""))
member.set(\.admin, to: memberInfo.isAdmin)
member.set(\.invited, to: (memberInfo.isAdmin ? 0 : 1)) // Admins can't be in the invited state
if
let picUrl: String = memberInfo.profile?.profilePictureUrl,
let picKey: Data = memberInfo.profile?.profileEncryptionKey,
!picUrl.isEmpty,
picKey.count == DisplayPictureManager.aes256KeyByteLength
{
member.set(\.profile_pic.url, to: picUrl)
member.set(\.profile_pic.key, to: picKey)
}
groups_members_set(groupMembersConf, &member)
try LibSessionError.throwIfNeeded(groupMembersConf)
}
// Now that the members have been loaded we need to trigger the initial key generation for the group
var pushResult: UnsafePointer<UInt8>? = nil
var pushResultLen: Int = 0
guard groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen) else {
throw LibSessionError.failedToRekeyGroup
}
// Now that everything has been populated correctly we can load the state into memory
dependencies.mutate(cache: .libSession) { cache in
groupState.forEach { variant, config in
cache.setConfig(for: variant, sessionId: groupSessionId, to: config)
}
}
return (
groupSessionId,
groupIdentityKeyPair,
groupState,
ClosedGroup(
threadId: groupSessionId.hexString,
name: name,
formationTimestamp: creationTimestamp,
displayPictureUrl: displayPictureUrl,
displayPictureFilename: displayPictureFilename,
displayPictureEncryptionKey: displayPictureEncryptionKey,
lastDisplayPictureUpdate: creationTimestamp,
shouldPoll: true,
groupIdentityPrivateKey: Data(groupIdentityKeyPair.secretKey),
invited: false
),
members
.filter { $0.id != userSessionId.hexString }
.map { memberId, info -> GroupMember in
GroupMember(
groupId: groupSessionId.hexString,
profileId: memberId,
role: .standard,
roleStatus: .pending,
isHidden: false
)
}
.appending(
GroupMember(
groupId: groupSessionId.hexString,
profileId: userSessionId.hexString,
role: .admin,
roleStatus: .accepted,
isHidden: false
)
)
)
}
static func removeGroupStateIfNeeded(
_ db: Database,
groupSessionId: SessionId,
using dependencies: Dependencies
) {
dependencies.mutate(cache: .libSession) { cache in
cache.removeConfigs(for: groupSessionId)
}
_ = try? ConfigDump
.filter(ConfigDump.Columns.sessionId == groupSessionId.hexString)
.deleteAll(db)
}
static func saveCreatedGroup(
_ db: Database,
group: ClosedGroup,
groupState: [ConfigDump.Variant: Config],
using dependencies: Dependencies
) throws {
// Create and save dumps for the configs
try dependencies.mutate(cache: .libSession) { cache in
try groupState.forEach { variant, config in
try cache.createDump(
config: config,
for: variant,
sessionId: SessionId(.group, hex: group.id),
timestampMs: Int64(floor(group.formationTimestamp * 1000))
)?.upsert(db)
}
}
// Add the new group to the USER_GROUPS config message
try LibSession.add(
db,
groupSessionId: group.id,
groupIdentityPrivateKey: group.groupIdentityPrivateKey,
name: group.name,
authData: group.authData,
joinedAt: group.formationTimestamp,
invited: (group.invited == true),
using: dependencies
)
}
@discardableResult static func createGroupState(
groupSessionId: SessionId,
userED25519KeyPair: KeyPair,
groupIdentityPrivateKey: Data?,
shouldLoadState: Bool,
using dependencies: Dependencies
) throws -> [ConfigDump.Variant: Config] {
var secretKey: [UInt8] = userED25519KeyPair.secretKey
var groupIdentityPublicKey: [UInt8] = groupSessionId.publicKey
// Create the new config objects
var groupKeysConf: UnsafeMutablePointer<config_group_keys>? = nil
var groupInfoConf: UnsafeMutablePointer<config_object>? = nil
var groupMembersConf: UnsafeMutablePointer<config_object>? = nil
var error: [CChar] = [CChar](repeating: 0, count: 256)
// It looks like C doesn't deal will passing pointers to null variables well so we need
// to explicitly pass 'nil' for the admin key in this case
switch groupIdentityPrivateKey {
case .some(let privateKeyData):
var groupIdentityPrivateKey: [UInt8] = Array(privateKeyData)
try groups_info_init(
&groupInfoConf,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
nil,
0,
&error
).orThrow(error: error)
try groups_members_init(
&groupMembersConf,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
nil,
0,
&error
).orThrow(error: error)
try groups_keys_init(
&groupKeysConf,
&secretKey,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
groupInfoConf,
groupMembersConf,
nil,
0,
&error
).orThrow(error: error)
case .none:
try groups_info_init(
&groupInfoConf,
&groupIdentityPublicKey,
nil,
nil,
0,
&error
).orThrow(error: error)
try groups_members_init(
&groupMembersConf,
&groupIdentityPublicKey,
nil,
nil,
0,
&error
).orThrow(error: error)
try groups_keys_init(
&groupKeysConf,
&secretKey,
&groupIdentityPublicKey,
nil,
groupInfoConf,
groupMembersConf,
nil,
0,
&error
).orThrow(error: error)
}
guard
let keysConf: UnsafeMutablePointer<config_group_keys> = groupKeysConf,
let infoConf: UnsafeMutablePointer<config_object> = groupInfoConf,
let membersConf: UnsafeMutablePointer<config_object> = groupMembersConf
else {
Log.error(.libSession, "Group config objects were null")
throw LibSessionError.unableToCreateConfigObject
}
// Define the config state map and load it into memory
let groupState: [ConfigDump.Variant: Config] = [
.groupKeys: .groupKeys(keysConf, info: infoConf, members: membersConf),
.groupInfo: .object(infoConf),
.groupMembers: .object(membersConf),
]
// Only load the state if specified (during initial group creation we want to
// load the state after populating the different configs incase invalid data
// was provided)
if shouldLoadState {
dependencies.mutate(cache: .libSession) { cache in
groupState.forEach { variant, config in
cache.setConfig(for: variant, sessionId: groupSessionId, to: config)
}
}
}
return groupState
}
static func isAdmin(
groupSessionId: SessionId,
using dependencies: Dependencies
) -> Bool {
return dependencies.mutate(cache: .libSession) { cache in
guard case .groupKeys(let conf, _, _) = cache.config(for: .groupKeys, sessionId: groupSessionId) else {
return false
}
return groups_keys_is_admin(conf)
}
}
}
internal extension LibSessionCacheType {
func removeGroupStateIfNeeded(
_ db: Database,
groupSessionId: SessionId
) {
removeConfigs(for: groupSessionId)
_ = try? ConfigDump
.filter(ConfigDump.Columns.sessionId == groupSessionId.hexString)
.deleteAll(db)
}
}
private extension Int32 {
func orThrow(error: [CChar]) throws {
guard self != 0 else { return }
Log.error(.libSession, "Unable to create group config objects: \(String(cString: error))")
throw LibSessionError.unableToCreateConfigObject
}
}
// MARK: - C Conformance
extension config_group_member: CAccessible & CMutable {}