mirror of https://github.com/oxen-io/session-ios
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.
468 lines
18 KiB
Swift
468 lines
18 KiB
Swift
2 years ago
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||
|
|
||
|
import UIKit
|
||
|
import GRDB
|
||
|
import SessionUIKit
|
||
|
import SessionSnodeKit
|
||
|
import SessionUtil
|
||
|
import SessionUtilitiesKit
|
||
|
|
||
1 year ago
|
// MARK: - Group Domains
|
||
|
|
||
11 months ago
|
public extension LibSession.Crypto.Domain {
|
||
1 year ago
|
static var kickedMessage: SessionUtil.Crypto.Domain = "SessionGroupKickedMessage" // stringlint:disable
|
||
|
}
|
||
|
|
||
2 years ago
|
// MARK: - Convenience
|
||
|
|
||
11 months ago
|
internal extension LibSession {
|
||
2 years ago
|
typealias CreatedGroupInfo = (
|
||
2 years ago
|
groupSessionId: SessionId,
|
||
2 years ago
|
identityKeyPair: KeyPair,
|
||
|
groupState: [ConfigDump.Variant: Config],
|
||
|
group: ClosedGroup,
|
||
|
members: [GroupMember]
|
||
|
)
|
||
|
|
||
2 years ago
|
static func createGroup(
|
||
|
_ db: Database,
|
||
|
name: String,
|
||
2 years ago
|
description: String?,
|
||
2 years ago
|
displayPictureUrl: String?,
|
||
|
displayPictureFilename: String?,
|
||
|
displayPictureEncryptionKey: Data?,
|
||
2 years ago
|
members: [(id: String, profile: Profile?)],
|
||
2 years ago
|
using dependencies: Dependencies
|
||
2 years ago
|
) throws -> CreatedGroupInfo {
|
||
2 years ago
|
guard
|
||
2 years ago
|
let groupIdentityKeyPair: KeyPair = dependencies[singleton: .crypto].generate(.ed25519KeyPair()),
|
||
2 years ago
|
let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db, using: dependencies)
|
||
2 years ago
|
else { throw MessageSenderError.noKeyPair }
|
||
|
|
||
2 years ago
|
// Prep the relevant details (reduce the members to ensure we don't accidentally insert duplicates)
|
||
2 years ago
|
let groupSessionId: SessionId = SessionId(.group, publicKey: groupIdentityKeyPair.publicKey)
|
||
2 years ago
|
let creationTimestamp: TimeInterval = TimeInterval(
|
||
2 years ago
|
Double(SnodeAPI.currentOffsetTimestampMs(using: dependencies)) / 1000
|
||
2 years ago
|
)
|
||
2 years ago
|
let userSessionId: SessionId = getUserSessionId(db, using: dependencies)
|
||
2 years ago
|
let currentUserProfile: Profile? = Profile.fetchOrCreateCurrentUser(db, using: dependencies)
|
||
2 years ago
|
|
||
|
// Create the new config objects
|
||
2 years ago
|
let groupState: [ConfigDump.Variant: Config] = try createGroupState(
|
||
|
groupSessionId: groupSessionId,
|
||
|
userED25519KeyPair: userED25519KeyPair,
|
||
|
groupIdentityPrivateKey: Data(groupIdentityKeyPair.secretKey),
|
||
2 years ago
|
initialMembers: members.filter { $0.id != userSessionId.hexString },
|
||
|
initialAdmin: (userSessionId.hexString, currentUserProfile),
|
||
2 years ago
|
shouldLoadState: false, // We manually load the state after populating the configs
|
||
|
using: dependencies
|
||
|
)
|
||
2 years ago
|
|
||
2 years ago
|
// Extract the conf objects from the state to load in the initial data
|
||
2 years ago
|
guard case .groupKeys(_, let groupInfoConf, _) = groupState[.groupKeys] else {
|
||
11 months ago
|
SNLog("[LibSession] Group config objects were null")
|
||
|
throw LibSessionError.unableToCreateConfigObject
|
||
2 years ago
|
}
|
||
2 years ago
|
|
||
|
// Set the initial values in the confs
|
||
2 years ago
|
var cGroupName: [CChar] = name.cArray.nullTerminated()
|
||
|
groups_info_set_name(groupInfoConf, &cGroupName)
|
||
2 years ago
|
groups_info_set_created(groupInfoConf, Int64(floor(creationTimestamp)))
|
||
|
|
||
2 years ago
|
if let groupDescription: String = description {
|
||
|
var cGroupDescription: [CChar] = groupDescription.cArray.nullTerminated()
|
||
|
groups_info_set_description(groupInfoConf, &cGroupDescription)
|
||
|
}
|
||
|
|
||
2 years ago
|
if
|
||
|
let displayPictureUrl: String = displayPictureUrl,
|
||
|
let displayPictureEncryptionKey: Data = displayPictureEncryptionKey
|
||
|
{
|
||
2 years ago
|
var displayPic: user_profile_pic = user_profile_pic()
|
||
|
displayPic.url = displayPictureUrl.toLibSession()
|
||
|
displayPic.key = displayPictureEncryptionKey.toLibSession()
|
||
|
groups_info_set_pic(groupInfoConf, displayPic)
|
||
|
}
|
||
|
|
||
2 years ago
|
// Now that everything has been populated correctly we can load the state into memory
|
||
2 years ago
|
dependencies.mutate(cache: .sessionUtil) { cache in
|
||
2 years ago
|
groupState.forEach { variant, config in
|
||
2 years ago
|
cache.setConfig(for: variant, sessionId: groupSessionId, to: config)
|
||
2 years ago
|
}
|
||
|
}
|
||
|
|
||
2 years ago
|
return (
|
||
2 years ago
|
groupSessionId,
|
||
2 years ago
|
groupIdentityKeyPair,
|
||
2 years ago
|
groupState,
|
||
2 years ago
|
ClosedGroup(
|
||
2 years ago
|
threadId: groupSessionId.hexString,
|
||
2 years ago
|
name: name,
|
||
|
formationTimestamp: creationTimestamp,
|
||
|
displayPictureUrl: displayPictureUrl,
|
||
|
displayPictureFilename: displayPictureFilename,
|
||
|
displayPictureEncryptionKey: displayPictureEncryptionKey,
|
||
|
lastDisplayPictureUpdate: creationTimestamp,
|
||
2 years ago
|
shouldPoll: true,
|
||
|
groupIdentityPrivateKey: Data(groupIdentityKeyPair.secretKey),
|
||
2 years ago
|
invited: false
|
||
2 years ago
|
),
|
||
2 years ago
|
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
|
||
|
)
|
||
2 years ago
|
)
|
||
|
)
|
||
|
}
|
||
|
|
||
2 years ago
|
static func removeGroupStateIfNeeded(
|
||
|
_ db: Database,
|
||
2 years ago
|
groupSessionId: SessionId,
|
||
2 years ago
|
using dependencies: Dependencies
|
||
|
) {
|
||
|
dependencies.mutate(cache: .sessionUtil) { cache in
|
||
2 years ago
|
cache.setConfig(for: .groupKeys, sessionId: groupSessionId, to: nil)
|
||
|
cache.setConfig(for: .groupInfo, sessionId: groupSessionId, to: nil)
|
||
|
cache.setConfig(for: .groupMembers, sessionId: groupSessionId, to: nil)
|
||
2 years ago
|
}
|
||
|
|
||
|
_ = try? ConfigDump
|
||
2 years ago
|
.filter(ConfigDump.Columns.sessionId == groupSessionId.hexString)
|
||
2 years ago
|
.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 groupState.forEach { variant, config in
|
||
|
try SessionUtil.createDump(
|
||
|
config: config,
|
||
|
for: variant,
|
||
2 years ago
|
sessionId: SessionId(.group, hex: group.id),
|
||
1 year ago
|
timestampMs: Int64(floor(group.formationTimestamp * 1000)),
|
||
|
using: dependencies
|
||
2 years ago
|
)?.upsert(db)
|
||
2 years ago
|
}
|
||
|
|
||
|
// Add the new group to the USER_GROUPS config message
|
||
|
try SessionUtil.add(
|
||
|
db,
|
||
2 years ago
|
groupSessionId: group.id,
|
||
2 years ago
|
groupIdentityPrivateKey: group.groupIdentityPrivateKey,
|
||
|
name: group.name,
|
||
|
authData: group.authData,
|
||
2 years ago
|
joinedAt: group.formationTimestamp,
|
||
2 years ago
|
invited: (group.invited == true),
|
||
2 years ago
|
using: dependencies
|
||
|
)
|
||
|
}
|
||
|
|
||
2 years ago
|
@discardableResult static func createGroupState(
|
||
2 years ago
|
groupSessionId: SessionId,
|
||
2 years ago
|
userED25519KeyPair: KeyPair,
|
||
2 years ago
|
groupIdentityPrivateKey: Data?,
|
||
2 years ago
|
initialMembers: [(id: String, profile: Profile?)] = [],
|
||
|
initialAdmin: (id: String, profile: Profile?)? = nil,
|
||
2 years ago
|
shouldLoadState: Bool,
|
||
2 years ago
|
using dependencies: Dependencies
|
||
|
) throws -> [ConfigDump.Variant: Config] {
|
||
|
var secretKey: [UInt8] = userED25519KeyPair.secretKey
|
||
2 years ago
|
var groupIdentityPublicKey: [UInt8] = groupSessionId.publicKey
|
||
2 years ago
|
|
||
|
// 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)
|
||
2 years ago
|
|
||
2 years ago
|
func loading(
|
||
2 years ago
|
admin: (id: String, profile: Profile?)?,
|
||
|
members: [(id: String, profile: Profile?)],
|
||
2 years ago
|
into membersConf: UnsafeMutablePointer<config_object>?
|
||
|
) throws {
|
||
|
guard !members.isEmpty else { return }
|
||
|
|
||
2 years ago
|
/// Store the admin data first
|
||
|
switch admin {
|
||
|
case .none: break
|
||
|
case .some((let id, let profile)):
|
||
|
try CExceptionHelper.performSafely {
|
||
|
var profilePic: user_profile_pic = user_profile_pic()
|
||
|
|
||
|
if
|
||
|
let picUrl: String = profile?.profilePictureUrl,
|
||
|
let picKey: Data = profile?.profileEncryptionKey,
|
||
|
!picUrl.isEmpty,
|
||
|
picKey.count == DisplayPictureManager.aes256KeyByteLength
|
||
|
{
|
||
|
profilePic.url = picUrl.toLibSession()
|
||
|
profilePic.key = picKey.toLibSession()
|
||
|
}
|
||
|
|
||
|
var member: config_group_member = config_group_member(
|
||
|
session_id: id.toLibSession(),
|
||
|
name: (profile?.name ?? "").toLibSession(),
|
||
|
profile_pic: profilePic,
|
||
|
admin: true,
|
||
|
invited: 0,
|
||
|
promoted: 0,
|
||
1 year ago
|
removed: 0,
|
||
2 years ago
|
supplement: false
|
||
|
)
|
||
|
|
||
|
groups_members_set(membersConf, &member)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Then store the initial members
|
||
|
struct MemberInfo: Hashable {
|
||
|
let id: String
|
||
|
let profile: Profile?
|
||
|
}
|
||
|
|
||
|
try members
|
||
|
.map { MemberInfo(id: $0.id, profile: $0.profile) }
|
||
|
.asSet()
|
||
|
.forEach { memberInfo in
|
||
|
var profilePic: user_profile_pic = user_profile_pic()
|
||
|
|
||
|
if
|
||
|
let picUrl: String = memberInfo.profile?.profilePictureUrl,
|
||
|
let picKey: Data = memberInfo.profile?.profileEncryptionKey,
|
||
|
!picUrl.isEmpty,
|
||
|
picKey.count == DisplayPictureManager.aes256KeyByteLength
|
||
|
{
|
||
|
profilePic.url = picUrl.toLibSession()
|
||
|
profilePic.key = picKey.toLibSession()
|
||
|
}
|
||
2 years ago
|
|
||
2 years ago
|
try CExceptionHelper.performSafely {
|
||
|
var member: config_group_member = config_group_member(
|
||
|
session_id: memberInfo.id.toLibSession(),
|
||
|
name: (memberInfo.profile?.name ?? "").toLibSession(),
|
||
|
profile_pic: profilePic,
|
||
|
admin: false,
|
||
|
invited: 1,
|
||
|
promoted: 0,
|
||
1 year ago
|
removed: 0,
|
||
2 years ago
|
supplement: false
|
||
|
)
|
||
|
|
||
|
groups_members_set(membersConf, &member)
|
||
|
}
|
||
2 years ago
|
}
|
||
|
}
|
||
|
|
||
2 years ago
|
// 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)
|
||
2 years ago
|
try loading(admin: initialAdmin, members: initialMembers, into: groupMembersConf)
|
||
2 years ago
|
|
||
2 years ago
|
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)
|
||
2 years ago
|
try loading(admin: initialAdmin, members: initialMembers, into: groupMembersConf)
|
||
2 years ago
|
|
||
2 years ago
|
try groups_keys_init(
|
||
|
&groupKeysConf,
|
||
|
&secretKey,
|
||
|
&groupIdentityPublicKey,
|
||
|
nil,
|
||
|
groupInfoConf,
|
||
|
groupMembersConf,
|
||
|
nil,
|
||
|
0,
|
||
|
&error
|
||
|
).orThrow(error: error)
|
||
|
}
|
||
2 years ago
|
|
||
|
guard
|
||
|
let keysConf: UnsafeMutablePointer<config_group_keys> = groupKeysConf,
|
||
|
let infoConf: UnsafeMutablePointer<config_object> = groupInfoConf,
|
||
|
let membersConf: UnsafeMutablePointer<config_object> = groupMembersConf
|
||
|
else {
|
||
11 months ago
|
SNLog("[LibSession] Group config objects were null")
|
||
|
throw LibSessionError.unableToCreateConfigObject
|
||
2 years ago
|
}
|
||
|
|
||
|
// 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),
|
||
|
]
|
||
|
|
||
2 years ago
|
// 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: .sessionUtil) { cache in
|
||
|
groupState.forEach { variant, config in
|
||
|
cache.setConfig(for: variant, sessionId: groupSessionId, to: config)
|
||
|
}
|
||
2 years ago
|
}
|
||
|
}
|
||
|
|
||
|
return groupState
|
||
|
}
|
||
|
|
||
1 year ago
|
static func isAdmin(
|
||
|
groupSessionId: SessionId,
|
||
|
using dependencies: Dependencies
|
||
|
) -> Bool {
|
||
|
return (try? dependencies[cache: .sessionUtil]
|
||
|
.config(for: .groupKeys, sessionId: groupSessionId)
|
||
|
.wrappedValue
|
||
|
.map { config in
|
||
11 months ago
|
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
|
||
1 year ago
|
|
||
|
return groups_keys_is_admin(conf)
|
||
|
})
|
||
|
.defaulting(to: false)
|
||
|
}
|
||
|
|
||
2 years ago
|
static func encrypt(
|
||
|
message: Data,
|
||
2 years ago
|
groupSessionId: SessionId,
|
||
2 years ago
|
using dependencies: Dependencies
|
||
|
) throws -> Data {
|
||
2 years ago
|
return try dependencies[cache: .sessionUtil]
|
||
2 years ago
|
.config(for: .groupKeys, sessionId: groupSessionId)
|
||
2 years ago
|
.wrappedValue
|
||
|
.map { config in
|
||
11 months ago
|
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
|
||
2 years ago
|
|
||
|
var maybeCiphertext: UnsafeMutablePointer<UInt8>? = nil
|
||
|
var ciphertextLen: Int = 0
|
||
|
groups_keys_encrypt_message(
|
||
|
conf,
|
||
|
Array(message),
|
||
|
message.count,
|
||
|
&maybeCiphertext,
|
||
|
&ciphertextLen
|
||
|
)
|
||
|
|
||
|
guard
|
||
|
ciphertextLen > 0,
|
||
|
let ciphertext: Data = maybeCiphertext
|
||
|
.map({ Data(bytes: $0, count: ciphertextLen) })
|
||
|
else { throw MessageSenderError.encryptionFailed }
|
||
|
|
||
|
return ciphertext
|
||
|
} ?? { throw MessageSenderError.encryptionFailed }()
|
||
2 years ago
|
}
|
||
|
|
||
|
static func decrypt(
|
||
|
ciphertext: Data,
|
||
2 years ago
|
groupSessionId: SessionId,
|
||
2 years ago
|
using dependencies: Dependencies
|
||
2 years ago
|
) throws -> (plaintext: Data, sender: String) {
|
||
|
return try dependencies[cache: .sessionUtil]
|
||
2 years ago
|
.config(for: .groupKeys, sessionId: groupSessionId)
|
||
2 years ago
|
.wrappedValue
|
||
|
.map { config -> (Data, String) in
|
||
11 months ago
|
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
|
||
2 years ago
|
|
||
|
var ciphertext: [UInt8] = Array(ciphertext)
|
||
2 years ago
|
var cSessionId: [CChar] = [CChar](repeating: 0, count: 67)
|
||
2 years ago
|
var maybePlaintext: UnsafeMutablePointer<UInt8>? = nil
|
||
|
var plaintextLen: Int = 0
|
||
2 years ago
|
let didDecrypt: Bool = groups_keys_decrypt_message(
|
||
2 years ago
|
conf,
|
||
|
&ciphertext,
|
||
|
ciphertext.count,
|
||
2 years ago
|
&cSessionId,
|
||
2 years ago
|
&maybePlaintext,
|
||
|
&plaintextLen
|
||
|
)
|
||
|
|
||
2 years ago
|
// If we got a reported failure then just stop here
|
||
|
guard didDecrypt else { throw MessageReceiverError.decryptionFailed }
|
||
|
|
||
|
// We need to manually free 'maybePlaintext' upon a successful decryption
|
||
|
defer { maybePlaintext?.deallocate() }
|
||
|
|
||
2 years ago
|
guard
|
||
|
plaintextLen > 0,
|
||
|
let plaintext: Data = maybePlaintext
|
||
|
.map({ Data(bytes: $0, count: plaintextLen) })
|
||
|
else { throw MessageReceiverError.decryptionFailed }
|
||
|
|
||
2 years ago
|
return (plaintext, String(cString: cSessionId))
|
||
2 years ago
|
} ?? { throw MessageReceiverError.decryptionFailed }()
|
||
2 years ago
|
}
|
||
2 years ago
|
}
|
||
|
|
||
|
private extension Int32 {
|
||
|
func orThrow(error: [CChar]) throws {
|
||
|
guard self != 0 else { return }
|
||
|
|
||
11 months ago
|
SNLog("[LibSession] Unable to create group config objects: \(String(cString: error))")
|
||
|
throw LibSessionError.unableToCreateConfigObject
|
||
2 years ago
|
}
|
||
|
}
|