Started working on some config contact pruning logic

Added support for a ConfirmationModal with an input field
Added a mechanism on Debug builds to export the database and it's key
Added logic to catch exceptions thrown within libSession (need to actually plug it in)
Added a debug-only mechanism to export the users database and (encrypted) database key
Added a few unit tests to check the CONTACTS config message size constraints
This commit is contained in:
Morgan Pretty 2023-06-16 19:38:14 +10:00
parent 65bf7d7d82
commit d2c82cb915
19 changed files with 2375 additions and 1530 deletions

View File

@ -0,0 +1,20 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CryptoKit
let arguments = CommandLine.arguments
// First argument is the file name
if arguments.count == 3 {
let encryptedData = Data(base64Encoded: arguments[1].data(using: .utf8)!)!
let hash: SHA256.Digest = SHA256.hash(data: arguments[2].data(using: .utf8)!)
let key: SymmetricKey = SymmetricKey(data: Data(hash.makeIterator()))
let sealedBox = try! ChaChaPoly.SealedBox(combined: encryptedData)
let decryptedData = try! ChaChaPoly.open(sealedBox, using: key)
print(Array(decryptedData).map { String(format: "%02x", $0) }.joined())
}
else {
print("Please provide the base64 encoded 'encrypted key' and plain text 'password' as arguments")
}

View File

@ -588,6 +588,8 @@
FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */; };
FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */; };
FD3003662A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */; };
FD30036A2A3ADEC100B5A5FB /* CExceptionHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */; settings = {ATTRIBUTES = (Public, ); }; };
FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */; };
FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; };
FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; };
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; };
@ -1727,6 +1729,8 @@
FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationSyncJob.swift; sourceTree = "<group>"; };
FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = "<group>"; };
FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMessageReceiveJob.swift; sourceTree = "<group>"; };
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CExceptionHelper.h; sourceTree = "<group>"; };
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CExceptionHelper.mm; sourceTree = "<group>"; };
FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = "<group>"; };
FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
@ -3589,6 +3593,8 @@
isa = PBXGroup;
children = (
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */,
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */,
FD09796A27F6C67500936362 /* Failable.swift */,
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
@ -4457,6 +4463,7 @@
B8856E1A256F1700001CE70E /* OWSMath.h in Headers */,
C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */,
C32C5B51256DC219003C73A2 /* NSNotificationCenter+OWS.h in Headers */,
FD30036A2A3ADEC100B5A5FB /* CExceptionHelper.h in Headers */,
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */,
B8856D8D256F1502001CE70E /* UIView+OWS.h in Headers */,
@ -5639,6 +5646,7 @@
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */,
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */,
FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */,
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */,

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CryptoKit
import GRDB
import DifferenceKit
import SessionUIKit
@ -9,6 +10,10 @@ import SessionUtilitiesKit
import SignalCoreKit
class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpViewModel.Section> {
#if DEBUG
private var databaseKeyEncryptionPassword: String = ""
#endif
// MARK: - Section
public enum Section: SessionTableSection {
@ -17,6 +22,9 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
case feedback
case faq
case support
#if DEBUG
case exportDatabase
#endif
var style: SessionTableSectionStyle { .padding }
}
@ -136,6 +144,28 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
]
)
]
#if DEBUG
.appending(
SectionModel(
model: .exportDatabase,
elements: [
SessionCell.Info(
id: .support,
title: "Export Database",
rightAccessory: .icon(
UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")?
.withRenderingMode(.alwaysTemplate),
size: .small
),
styling: SessionCell.StyleInfo(
tintColor: .danger
),
onTapView: { [weak self] view in self?.exportDatabase(view) }
)
]
)
)
#endif
}
.removeDuplicates()
.publisher(in: Storage.shared)
@ -197,4 +227,132 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
showShareSheet()
}
}
#if DEBUG
private func exportDatabase(_ targetView: UIView?) {
let generatedPassword: String = UUID().uuidString
self.databaseKeyEncryptionPassword = generatedPassword
self.transitionToScreen(
ConfirmationModal(
info: ConfirmationModal.Info(
title: "Export Database",
body: .input(
explanation: NSAttributedString(
string: """
Sharing the database and key together is dangerous!
We've generated a secure password for you but feel free to provide your own (we will show the generated password again after exporting)
This password will be used to encrypt the database decryption key and will be exported alongside the database
"""
),
placeholder: "Enter a password",
initialValue: generatedPassword,
clearButton: true,
onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value }
),
confirmTitle: "Export",
dismissOnConfirm: false,
onConfirm: { [weak self] modal in
modal.dismiss(animated: true) {
guard let password: String = self?.databaseKeyEncryptionPassword, password.count >= 6 else {
self?.transitionToScreen(
ConfirmationModal(
info: ConfirmationModal.Info(
title: "Error",
body: .text("Password must be at least 6 characters")
)
),
transitionType: .present
)
return
}
do {
let exportInfo = try Storage.shared.exportInfo(password: password)
let shareVC = UIActivityViewController(
activityItems: [
URL(fileURLWithPath: exportInfo.dbPath),
URL(fileURLWithPath: exportInfo.keyPath)
],
applicationActivities: nil
)
shareVC.completionWithItemsHandler = { [weak self] _, completed, _, _ in
guard
completed &&
generatedPassword == self?.databaseKeyEncryptionPassword
else { return }
self?.transitionToScreen(
ConfirmationModal(
info: ConfirmationModal.Info(
title: "Password",
body: .text("""
The generated password was:
\(generatedPassword)
Avoid sending this via the same means as the database
"""),
confirmTitle: "Share",
dismissOnConfirm: false,
onConfirm: { [weak self] modal in
modal.dismiss(animated: true) {
let passwordShareVC = UIActivityViewController(
activityItems: [generatedPassword],
applicationActivities: nil
)
if UIDevice.current.isIPad {
passwordShareVC.excludedActivityTypes = []
passwordShareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : [])
passwordShareVC.popoverPresentationController?.sourceView = targetView
passwordShareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero)
}
self?.transitionToScreen(passwordShareVC, transitionType: .present)
}
}
)
),
transitionType: .present
)
}
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : [])
shareVC.popoverPresentationController?.sourceView = targetView
shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero)
}
self?.transitionToScreen(shareVC, transitionType: .present)
}
catch {
let message: String = {
switch error {
case CryptoKitError.incorrectKeySize:
return "The password must be between 6 and 32 characters (padded to 32 bytes)"
default: return "Failed to export database"
}
}()
self?.transitionToScreen(
ConfirmationModal(
info: ConfirmationModal.Info(
title: "Error",
body: .text(message)
)
),
transitionType: .present
)
}
}
}
)
),
transitionType: .present
)
}
#endif
}

View File

@ -55,11 +55,9 @@ enum _013_SessionUtilChanges: Migration {
// shares the same 'id' as the 'groupId') so we can cascade delete automatically
t.column(.groupId, .text)
.notNull()
.indexed() // Quicker querying
.references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted
t.column(.profileId, .text)
.notNull()
.indexed() // Quicker querying
t.column(.role, .integer).notNull()
t.column(.isHidden, .boolean)
.notNull()
@ -80,6 +78,11 @@ enum _013_SessionUtilChanges: Migration {
try db.drop(table: GroupMember.self)
try db.rename(table: TmpGroupMember.databaseTableName, to: GroupMember.databaseTableName)
// Need to create the indexes separately from creating 'TmpGroupMember' to ensure they
// have the correct names
try db.createIndex(on: GroupMember.self, columns: [.groupId])
try db.createIndex(on: GroupMember.self, columns: [.profileId])
// SQLite doesn't support removing unique constraints so we need to create a new table with
// the setup we want, copy data from the old table over, drop the old table and rename the new table
struct TmpClosedGroupKeyPair: Codable, TableRecord, FetchableRecord, PersistableRecord, ColumnExpressible {
@ -107,18 +110,16 @@ enum _013_SessionUtilChanges: Migration {
try db.create(table: TmpClosedGroupKeyPair.self) { t in
t.column(.threadId, .text)
.notNull()
.indexed() // Quicker querying
.references(ClosedGroup.self, onDelete: .cascade) // Delete if ClosedGroup deleted
t.column(.publicKey, .blob).notNull()
t.column(.secretKey, .blob).notNull()
t.column(.receivedTimestamp, .double)
.notNull()
.indexed() // Quicker querying
t.column(.threadKeyPairHash, .integer)
.notNull()
.unique()
.indexed()
}
// Insert into the new table, drop the old table and rename the new table to be the old one
try ClosedGroupKeyPair
.fetchAll(db)
@ -144,6 +145,12 @@ enum _013_SessionUtilChanges: Migration {
try db.rename(table: TmpClosedGroupKeyPair.databaseTableName, to: ClosedGroupKeyPair.databaseTableName)
// Add an index for the 'ClosedGroupKeyPair' so we can lookup existing keys more easily
//
// Note: Need to create the indexes separately from creating 'TmpClosedGroupKeyPair' to ensure they
// have the correct names
try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.threadId])
try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.receivedTimestamp])
try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.threadKeyPairHash])
try db.createIndex(
on: ClosedGroupKeyPair.self,
columns: [.threadId, .threadKeyPairHash]

View File

@ -54,12 +54,13 @@ public struct Contact: Codable, Identifiable, Equatable, FetchableRecord, Persis
isApproved: Bool = false,
isBlocked: Bool = false,
didApproveMe: Bool = false,
hasBeenBlocked: Bool = false
hasBeenBlocked: Bool = false,
dependencies: Dependencies = Dependencies()
) {
self.id = id
self.isTrusted = (
isTrusted ||
id == getUserHexEncodedPublicKey() // Always trust ourselves
id == getUserHexEncodedPublicKey(dependencies: dependencies) // Always trust ourselves
)
self.isApproved = isApproved
self.isBlocked = isBlocked

View File

@ -36,7 +36,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
case pinnedPriority
}
public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible {
public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible, CaseIterable {
case contact
case legacyGroup
case community

View File

@ -34,63 +34,16 @@ internal extension SessionUtil {
mergeNeedsDump: Bool,
latestConfigSentTimestampMs: Int64
) throws {
typealias ContactData = [
String: (
contact: Contact,
profile: Profile,
priority: Int32,
created: TimeInterval
)
]
guard mergeNeedsDump else { return }
guard conf != nil else { throw SessionUtilError.nilConfigObject }
var contactData: ContactData = [:]
var contact: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact) {
let contactId: String = String(cString: withUnsafeBytes(of: contact.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
let contactResult: Contact = Contact(
id: contactId,
isApproved: contact.approved,
isBlocked: contact.blocked,
didApproveMe: contact.approved_me
)
let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true)
let profileResult: Profile = Profile(
id: contactId,
name: String(libSessionVal: contact.name),
lastNameUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000),
nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true),
profilePictureUrl: profilePictureUrl,
profileEncryptionKey: (profilePictureUrl == nil ? nil :
Data(
libSessionVal: contact.profile_pic.key,
count: ProfileManager.avatarAES256KeyByteLength
)
),
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000)
)
contactData[contactId] = (
contactResult,
profileResult,
contact.priority,
TimeInterval(contact.created)
)
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
// The current users contact data is handled separately so exclude it if it's present (as that's
// actually a bug)
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let targetContactData: ContactData = contactData.filter { $0.key != userPublicKey }
let targetContactData: [String: ContactData] = extractContacts(
from: conf,
latestConfigSentTimestampMs: latestConfigSentTimestampMs
).filter { $0.key != userPublicKey }
// Since we don't sync 100% of the data stored against the contact and profile objects we
// need to only update the data we do have to ensure we don't overwrite anything that doesn't
@ -490,6 +443,121 @@ internal extension SessionUtil {
return updated
}
// MARK: - Pruning
static func pruningIfNeeded(
_ db: Database,
conf: UnsafeMutablePointer<config_object>?
) throws {
// First make sure we are actually thowing the correct size constraint error (don't want to prune contacts
// as a result of some other random error
do {
try CExceptionHelper.performSafely { config_push(conf).deallocate() }
return // If we didn't error then no need to prune
}
catch {
guard (error as NSError).userInfo["NSLocalizedDescription"] as? String == "Config data is too large" else {
throw error
}
}
// Extract the contact data from the config
var allContactData: [String: ContactData] = extractContacts(
from: conf,
latestConfigSentTimestampMs: 0
)
// Remove the current user profile info (shouldn't be in there but just in case)
let userPublicKey: String = getUserHexEncodedPublicKey(db)
var cUserPublicKey: [CChar] = userPublicKey.cArray.nullTerminated()
contacts_erase(conf, &cUserPublicKey)
/// Do the following in stages (we want to prune as few contacts as possible because we are essentially deleting data and removing these
/// contacts will result in not just contact data but also associated conversation data for the contact being removed from linked devices
///
///
/// **Step 1** First of all we want to try to detect spam-attacks (ie. if someone creates a bunch of accounts and messages you, and you
/// systematically block every one of those accounts - this can quickly add up)
///
/// We will do this by filtering the contact data to only include blocked contacts, grouping those contacts into contacts created within the
/// same hour and then only including groups that have more than 10 contacts (ie. if you blocked 20 users within an hour we expect those
/// contacts were spammers)
let blockSpamBatchingResolution: TimeInterval = (60 * 60)
// TODO: Do we want to only do this case for contacts that were created over X time ago? (to avoid unintentionally unblocking accounts that were recently blocked
let likelySpammerContacts: [ContactData] = allContactData
.values
.filter { $0.contact.isBlocked }
.grouped(by: { $0.created / blockSpamBatchingResolution })
.filter { _, values in values.count > 20 }
.values
.flatMap { $0 }
if !likelySpammerContacts.isEmpty {
likelySpammerContacts.forEach { contact in
var cSessionId: [CChar] = contact.contact.id.cArray.nullTerminated()
contacts_erase(conf, &cSessionId)
allContactData.removeValue(forKey: contact.contact.id)
}
// If we are no longer erroring then we can stop here
do { return try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch {}
}
/// We retrieve the `CONVO_INFO_VOLATILE` records and one-to-one conversation message counts as they will be relevant for subsequent checks
let volatileThreadInfo: [String: VolatileThreadInfo] = SessionUtil
.config(for: .convoInfoVolatile, publicKey: userPublicKey)
.wrappedValue
.map { SessionUtil.extractConvoVolatileInfo(from: $0) }
.defaulting(to: [])
.reduce(into: [:]) { result, next in result[next.threadId] = next }
let conversationMessageCounts: [String: Int] = try SessionThread
.filter(SessionThread.Columns.variant == SessionThread.Variant.contact)
.select(.id)
.annotated(with: SessionThread.interactions.count)
.asRequest(of: ThreadCount.self)
.fetchAll(db)
.reduce(into: [:]) { result, next in result[next.id] = next.interactionCount }
/// **Step 2** Next up we want to remove contact records which are likely to be invalid, this means contacts which:
/// - Aren't blocked
/// - Have no `name` value
/// - Have no `CONVO_INFO_VOLATILE` record
/// - Have no messages in their one-to-one conversations
///
/// Any contacts that meet the above criteria are likely either invalid contacts or are contacts which the user hasn't seen or interacted
/// with for 30+ days
let likelyInvalidContacts: [ContactData] = allContactData
.values
.filter { !$0.contact.isBlocked }
.filter { $0.profile.name.isEmpty }
.filter { volatileThreadInfo[$0.contact.id] == nil }
.filter { (conversationMessageCounts[$0.contact.id] ?? 0) == 0 }
if !likelyInvalidContacts.isEmpty {
likelyInvalidContacts.forEach { contact in
var cSessionId: [CChar] = contact.contact.id.cArray.nullTerminated()
contacts_erase(conf, &cSessionId)
allContactData.removeValue(forKey: contact.contact.id)
}
// If we are no longer erroring then we can stop here
do { return try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch {}
}
// TODO: Exclude contacts that have no profile info(?)
// TODO: Exclude contacts that have a CONVO_INFO_VOLATILE record
// TODO: Exclude contacts that have a conversation with messages in the database (ie. only delete "empty" contacts)
// TODO: Start pruning valid contacts which have really old conversations...
print("RAWR")
}
}
// MARK: - External Outgoing Changes
@ -555,3 +623,71 @@ extension SessionUtil {
}
}
}
// MARK: - ContactData
private struct ContactData {
let contact: Contact
let profile: Profile
let priority: Int32
let created: TimeInterval
}
// MARK: - ThreadCount
private struct ThreadCount: Codable, FetchableRecord {
let id: String
let interactionCount: Int
}
// MARK: - Convenience
private extension SessionUtil {
static func extractContacts(
from conf: UnsafeMutablePointer<config_object>?,
latestConfigSentTimestampMs: Int64
) -> [String: ContactData] {
var result: [String: ContactData] = [:]
var contact: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact) {
let contactId: String = String(cString: withUnsafeBytes(of: contact.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
let contactResult: Contact = Contact(
id: contactId,
isApproved: contact.approved,
isBlocked: contact.blocked,
didApproveMe: contact.approved_me
)
let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true)
let profileResult: Profile = Profile(
id: contactId,
name: String(libSessionVal: contact.name),
lastNameUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000),
nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true),
profilePictureUrl: profilePictureUrl,
profileEncryptionKey: (profilePictureUrl == nil ? nil :
Data(
libSessionVal: contact.profile_pic.key,
count: ProfileManager.avatarAES256KeyByteLength
)
),
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000)
)
result[contactId] = ContactData(
contact: contactResult,
profile: profileResult,
priority: contact.priority,
created: TimeInterval(contact.created)
)
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
return result
}
}

View File

@ -22,71 +22,8 @@ internal extension SessionUtil {
guard mergeNeedsDump else { return }
guard conf != nil else { throw SessionUtilError.nilConfigObject }
var volatileThreadInfo: [VolatileThreadInfo] = []
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var community: convo_info_volatile_community = convo_info_volatile_community()
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
while !convo_info_volatile_iterator_done(convoIterator) {
if convo_info_volatile_it_is_1to1(convoIterator, &oneToOne) {
volatileThreadInfo.append(
VolatileThreadInfo(
threadId: String(libSessionVal: oneToOne.session_id),
variant: .contact,
changes: [
.markedAsUnread(oneToOne.unread),
.lastReadTimestampMs(oneToOne.last_read)
]
)
)
}
else if convo_info_volatile_it_is_community(convoIterator, &community) {
let server: String = String(libSessionVal: community.base_url)
let roomToken: String = String(libSessionVal: community.room)
let publicKey: String = Data(
libSessionVal: community.pubkey,
count: OpenGroup.pubkeyByteLength
).toHexString()
volatileThreadInfo.append(
VolatileThreadInfo(
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
variant: .community,
openGroupUrlInfo: OpenGroupUrlInfo(
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
server: server,
roomToken: roomToken,
publicKey: publicKey
),
changes: [
.markedAsUnread(community.unread),
.lastReadTimestampMs(community.last_read)
]
)
)
}
else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) {
volatileThreadInfo.append(
VolatileThreadInfo(
threadId: String(libSessionVal: legacyGroup.group_id),
variant: .legacyGroup,
changes: [
.markedAsUnread(legacyGroup.unread),
.lastReadTimestampMs(legacyGroup.last_read)
]
)
)
}
else {
SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update")
}
convo_info_volatile_iterator_advance(convoIterator)
}
convo_info_volatile_iterator_free(convoIterator) // Need to free the iterator
// Get the local volatile thread info from all conversations
// Get the volatile thread info from the conf and local conversations
let volatileThreadInfo: [VolatileThreadInfo] = extractConvoVolatileInfo(from: conf)
let localVolatileThreadInfo: [String: VolatileThreadInfo] = VolatileThreadInfo.fetchAll(db)
.reduce(into: [:]) { result, next in result[next.threadId] = next }
@ -572,6 +509,76 @@ public extension SessionUtil {
}
}
}
internal static func extractConvoVolatileInfo(
from conf: UnsafeMutablePointer<config_object>?
) -> [VolatileThreadInfo] {
var result: [VolatileThreadInfo] = []
var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var community: convo_info_volatile_community = convo_info_volatile_community()
var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
while !convo_info_volatile_iterator_done(convoIterator) {
if convo_info_volatile_it_is_1to1(convoIterator, &oneToOne) {
result.append(
VolatileThreadInfo(
threadId: String(libSessionVal: oneToOne.session_id),
variant: .contact,
changes: [
.markedAsUnread(oneToOne.unread),
.lastReadTimestampMs(oneToOne.last_read)
]
)
)
}
else if convo_info_volatile_it_is_community(convoIterator, &community) {
let server: String = String(libSessionVal: community.base_url)
let roomToken: String = String(libSessionVal: community.room)
let publicKey: String = Data(
libSessionVal: community.pubkey,
count: OpenGroup.pubkeyByteLength
).toHexString()
result.append(
VolatileThreadInfo(
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
variant: .community,
openGroupUrlInfo: OpenGroupUrlInfo(
threadId: OpenGroup.idFor(roomToken: roomToken, server: server),
server: server,
roomToken: roomToken,
publicKey: publicKey
),
changes: [
.markedAsUnread(community.unread),
.lastReadTimestampMs(community.last_read)
]
)
)
}
else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) {
result.append(
VolatileThreadInfo(
threadId: String(libSessionVal: legacyGroup.group_id),
variant: .legacyGroup,
changes: [
.markedAsUnread(legacyGroup.unread),
.lastReadTimestampMs(legacyGroup.last_read)
]
)
)
}
else {
SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update")
}
convo_info_volatile_iterator_advance(convoIterator)
}
convo_info_volatile_iterator_free(convoIterator) // Need to free the iterator
return result
}
}
fileprivate extension [SessionUtil.VolatileThreadInfo.Change] {
@ -597,3 +604,4 @@ fileprivate extension [SessionUtil.VolatileThreadInfo.Change] {
return nil
}
}

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtil
import SessionUtilitiesKit
@ -8,299 +9,601 @@ import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigContactsSpec {
enum ContactProperty: CaseIterable {
case name
case nickname
case approved
case approved_me
case blocked
case profile_pic
case created
case notifications
case mute_until
}
// MARK: - Spec
static func spec() {
it("generates Contact configs correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse())
expect(contacts_size(conf)).to(equal(0))
var contact2: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact2.name)).to(beEmpty())
expect(String(libSessionVal: contact2.nickname)).to(beEmpty())
expect(contact2.approved).to(beFalse())
expect(contact2.approved_me).to(beFalse())
expect(contact2.blocked).to(beFalse())
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty())
expect(contact2.created).to(equal(0))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(contact2.mute_until).to(equal(0))
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
pushData1.deallocate()
// Update the contact data
contact2.name = "Joe".toLibSession()
contact2.nickname = "Joey".toLibSession()
contact2.approved = true
contact2.approved_me = true
contact2.created = createdTs
contact2.notifications = CONVO_NOTIFY_ALL
contact2.mute_until = nowTs + 1800
// Update the contact
contacts_set(conf, &contact2)
// Ensure the contact details were updated
var contact3: contacts_contact = contacts_contact()
expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact3.name)).to(equal("Joe"))
expect(String(libSessionVal: contact3.nickname)).to(equal("Joey"))
expect(contact3.approved).to(beTrue())
expect(contact3.approved_me).to(beTrue())
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty())
expect(contact3.blocked).to(beFalse())
expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId))
expect(contact3.created).to(equal(createdTs))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(contact2.mute_until).to(equal(nowTs + 1800))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
expect(pushData2.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue())
pushData2.deallocate()
// NB: Not going to check encrypted data and decryption here because that's general (not
// specific to contacts) and is covered already in the user profile tests.
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData3.pointee.seqno).to(equal(1))
pushData3.deallocate()
// Because we just called dump() above, to load up contacts2
expect(config_needs_dump(conf)).to(beFalse())
// Ensure the contact details were updated
var contact4: contacts_contact = contacts_contact()
expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact4.name)).to(equal("Joe"))
expect(String(libSessionVal: contact4.nickname)).to(equal("Joey"))
expect(contact4.approved).to(beTrue())
expect(contact4.approved_me).to(beTrue())
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty())
expect(contact4.blocked).to(beFalse())
expect(contact4.created).to(equal(createdTs))
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var contact5: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
expect(String(libSessionVal: contact5.name)).to(beEmpty())
expect(String(libSessionVal: contact5.nickname)).to(beEmpty())
expect(contact5.approved).to(beFalse())
expect(contact5.approved_me).to(beFalse())
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty())
expect(contact5.blocked).to(beFalse())
// We're not setting any fields, but we should still keep a record of the session id
contacts_set(conf2, &contact5)
expect(config_needs_push(conf2)).to(beTrue())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2)
mergeHashes.forEach { $0?.deallocate() }
pushData4.deallocate()
expect(config_needs_push(conf)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData5.pointee.seqno).to(equal(2))
pushData5.deallocate()
// Iterate through and make sure we got everything we expected
var sessionIds: [String] = []
var nicknames: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact6: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact6) {
sessionIds.append(String(libSessionVal: contact6.session_id))
nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator)
context("CONTACTS") {
// MARK: - when checking error catching
context("when checking error catching") {
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: UnsafeMutablePointer<CChar>?
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = nil
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, error)
error?.deallocate()
}
// MARK: -- it can catch size limit errors thrown when pushing
it("can catch size limit errors thrown when pushing") {
try (0..<10000).forEach { index in
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties)
contacts_set(conf, &contact)
}
expect(contacts_size(conf)).to(equal(10000))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
expect {
try CExceptionHelper.performSafely { config_push(conf).deallocate() }
}
.to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"])))
}
// MARK: -- can catch size limit errors thrown when dumping
it("can catch size limit errors thrown when dumping") {
try (0..<10000).forEach { index in
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties)
contacts_set(conf, &contact)
}
expect(contacts_size(conf)).to(equal(10000))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
expect {
try CExceptionHelper.performSafely {
var dump: UnsafeMutablePointer<UInt8>? = nil
var dumpLen: Int = 0
config_dump(conf, &dump, &dumpLen)
dump?.deallocate()
}
}
.to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"])))
}
}
contacts_iterator_free(contactIterator) // Need to free the iterator
expect(sessionIds.count).to(equal(2))
expect(sessionIds.count).to(equal(contacts_size(conf)))
expect(sessionIds.first).to(equal(definitelyRealId))
expect(sessionIds.last).to(equal(anotherId))
expect(nicknames.first).to(equal("Joey"))
expect(nicknames.last).to(equal("(N/A)"))
// Conflict! Oh no!
// On client 1 delete a contact:
contacts_erase(conf, definitelyRealId)
// Client 2 adds a new friend:
let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var contact7: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
contact7.nickname = "Nickname 3".toLibSession()
contact7.approved = true
contact7.approved_me = true
contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession()
contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
contacts_set(conf2, &contact7)
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData6.pointee.seqno).to(equal(3))
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(3))
let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)!
let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)!
expect(pushData6Str).toNot(equal(pushData7Str))
expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len))
.to(equal([fakeHash2]))
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash2]))
let fakeHash3a: String = "fakehash3a"
var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated()
let fakeHash3b: String = "fakehash3b"
var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated()
config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a)
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b)
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3b].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData7.pointee.config)]
var mergeSize2: [Int] = [pushData7.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(config_needs_push(conf)).to(beTrue())
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3a].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData6.pointee.config)]
var mergeSize3: [Int] = [pushData6.pointee.config_len]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beTrue())
mergeHashes2.forEach { $0?.deallocate() }
mergeHashes3.forEach { $0?.deallocate() }
pushData6.deallocate()
pushData7.deallocate()
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData8.pointee.seqno).to(equal(4))
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno))
let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)!
let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)!
expect(pushData8Str).to(equal(pushData9Str))
expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len))
.to(equal([fakeHash3b, fakeHash3a]))
expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len))
.to(equal([fakeHash3a, fakeHash3b]))
let fakeHash4: String = "fakeHash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4)
pushData8.deallocate()
pushData9.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Validate the changes
var sessionIds2: [String] = []
var nicknames2: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact8: contacts_contact = contacts_contact()
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator2, &contact8) {
sessionIds2.append(String(libSessionVal: contact8.session_id))
nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator2)
// MARK: - when checking size limits
context("when checking size limits") {
var numRecords: Int!
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: UnsafeMutablePointer<CChar>?
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
numRecords = 0
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = nil
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, error)
error?.deallocate()
}
// MARK: -- has not changed the max empty records
it("has not changed the max empty records") {
for index in (0..<10000) {
var contact: contacts_contact = try createContact(for: index, in: conf)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(1775))
}
// MARK: -- has not changed the max name only records
it("has not changed the max name only records") {
for index in (0..<10000) {
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name])
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(526))
}
// MARK: -- has not changed the max name and profile pic only records
it("has not changed the max name and profile pic only records") {
for index in (0..<10000) {
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name, .profile_pic])
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(184))
}
// MARK: -- has not changed the max filled records
it("has not changed the max filled records") {
for index in (0..<10000) {
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(134))
}
}
contacts_iterator_free(contactIterator2) // Need to free the iterator
expect(sessionIds2.count).to(equal(2))
expect(sessionIds2.first).to(equal(anotherId))
expect(sessionIds2.last).to(equal(thirdId))
expect(nicknames2.first).to(equal("(N/A)"))
expect(nicknames2.last).to(equal("Nickname 3"))
// MARK: - when pruning
context("when pruning") {
var mockStorage: Storage!
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: UnsafeMutablePointer<CChar>?
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
mockStorage = Storage(
customWriter: try! DatabaseQueue(),
customMigrations: [
SNUtilitiesKit.migrations(),
SNMessagingKit.migrations()
]
)
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = nil
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, error)
error?.deallocate()
}
it("does something") {
mockStorage.write { db in
try SessionThread.fetchOrCreate(db, id: "1", variant: .contact, shouldBeVisible: true)
try SessionThread.fetchOrCreate(db, id: "2", variant: .contact, shouldBeVisible: true)
try SessionThread.fetchOrCreate(db, id: "3", variant: .contact, shouldBeVisible: true)
_ = try Interaction(
threadId: "1",
authorId: "1",
variant: .standardIncoming,
body: "Test1"
).inserted(db)
_ = try Interaction(
threadId: "1",
authorId: "2",
variant: .standardIncoming,
body: "Test2"
).inserted(db)
_ = try Interaction(
threadId: "3",
authorId: "3",
variant: .standardIncoming,
body: "Test3"
).inserted(db)
try SessionUtil.pruningIfNeeded(
db,
conf: conf
)
expect(contacts_size(conf)).to(equal(0))
}
}
}
// MARK: - generates config correctly
it("generates config correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse())
expect(contacts_size(conf)).to(equal(0))
var contact2: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact2.name)).to(beEmpty())
expect(String(libSessionVal: contact2.nickname)).to(beEmpty())
expect(contact2.approved).to(beFalse())
expect(contact2.approved_me).to(beFalse())
expect(contact2.blocked).to(beFalse())
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty())
expect(contact2.created).to(equal(0))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(contact2.mute_until).to(equal(0))
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
pushData1.deallocate()
// Update the contact data
contact2.name = "Joe".toLibSession()
contact2.nickname = "Joey".toLibSession()
contact2.approved = true
contact2.approved_me = true
contact2.created = createdTs
contact2.notifications = CONVO_NOTIFY_ALL
contact2.mute_until = nowTs + 1800
// Update the contact
contacts_set(conf, &contact2)
// Ensure the contact details were updated
var contact3: contacts_contact = contacts_contact()
expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact3.name)).to(equal("Joe"))
expect(String(libSessionVal: contact3.nickname)).to(equal("Joey"))
expect(contact3.approved).to(beTrue())
expect(contact3.approved_me).to(beTrue())
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty())
expect(contact3.blocked).to(beFalse())
expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId))
expect(contact3.created).to(equal(createdTs))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(contact2.mute_until).to(equal(nowTs + 1800))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
expect(pushData2.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue())
pushData2.deallocate()
// NB: Not going to check encrypted data and decryption here because that's general (not
// specific to contacts) and is covered already in the user profile tests.
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData3.pointee.seqno).to(equal(1))
pushData3.deallocate()
// Because we just called dump() above, to load up contacts2
expect(config_needs_dump(conf)).to(beFalse())
// Ensure the contact details were updated
var contact4: contacts_contact = contacts_contact()
expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact4.name)).to(equal("Joe"))
expect(String(libSessionVal: contact4.nickname)).to(equal("Joey"))
expect(contact4.approved).to(beTrue())
expect(contact4.approved_me).to(beTrue())
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty())
expect(contact4.blocked).to(beFalse())
expect(contact4.created).to(equal(createdTs))
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var contact5: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
expect(String(libSessionVal: contact5.name)).to(beEmpty())
expect(String(libSessionVal: contact5.nickname)).to(beEmpty())
expect(contact5.approved).to(beFalse())
expect(contact5.approved_me).to(beFalse())
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty())
expect(contact5.blocked).to(beFalse())
// We're not setting any fields, but we should still keep a record of the session id
contacts_set(conf2, &contact5)
expect(config_needs_push(conf2)).to(beTrue())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2)
mergeHashes.forEach { $0?.deallocate() }
pushData4.deallocate()
expect(config_needs_push(conf)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData5.pointee.seqno).to(equal(2))
pushData5.deallocate()
// Iterate through and make sure we got everything we expected
var sessionIds: [String] = []
var nicknames: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact6: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact6) {
sessionIds.append(String(libSessionVal: contact6.session_id))
nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
expect(sessionIds.count).to(equal(2))
expect(sessionIds.count).to(equal(contacts_size(conf)))
expect(sessionIds.first).to(equal(definitelyRealId))
expect(sessionIds.last).to(equal(anotherId))
expect(nicknames.first).to(equal("Joey"))
expect(nicknames.last).to(equal("(N/A)"))
// Conflict! Oh no!
// On client 1 delete a contact:
contacts_erase(conf, definitelyRealId)
// Client 2 adds a new friend:
let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var contact7: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
contact7.nickname = "Nickname 3".toLibSession()
contact7.approved = true
contact7.approved_me = true
contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession()
contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
contacts_set(conf2, &contact7)
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData6.pointee.seqno).to(equal(3))
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(3))
let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)!
let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)!
expect(pushData6Str).toNot(equal(pushData7Str))
expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len))
.to(equal([fakeHash2]))
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash2]))
let fakeHash3a: String = "fakehash3a"
var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated()
let fakeHash3b: String = "fakehash3b"
var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated()
config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a)
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b)
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3b].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData7.pointee.config)]
var mergeSize2: [Int] = [pushData7.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(config_needs_push(conf)).to(beTrue())
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3a].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData6.pointee.config)]
var mergeSize3: [Int] = [pushData6.pointee.config_len]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beTrue())
mergeHashes2.forEach { $0?.deallocate() }
mergeHashes3.forEach { $0?.deallocate() }
pushData6.deallocate()
pushData7.deallocate()
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData8.pointee.seqno).to(equal(4))
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno))
let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)!
let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)!
expect(pushData8Str).to(equal(pushData9Str))
expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len))
.to(equal([fakeHash3b, fakeHash3a]))
expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len))
.to(equal([fakeHash3a, fakeHash3b]))
let fakeHash4: String = "fakeHash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4)
pushData8.deallocate()
pushData9.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Validate the changes
var sessionIds2: [String] = []
var nicknames2: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact8: contacts_contact = contacts_contact()
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator2, &contact8) {
sessionIds2.append(String(libSessionVal: contact8.session_id))
nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator2)
}
contacts_iterator_free(contactIterator2) // Need to free the iterator
expect(sessionIds2.count).to(equal(2))
expect(sessionIds2.first).to(equal(anotherId))
expect(sessionIds2.last).to(equal(thirdId))
expect(nicknames2.first).to(equal("(N/A)"))
expect(nicknames2.last).to(equal("Nickname 3"))
}
}
}
// MARK: - Convenience
private static func createContact(
for index: Int,
in conf: UnsafeMutablePointer<config_object>?,
maxing properties: [ContactProperty] = []
) throws -> contacts_contact {
let postPrefixId: String = "050000000000000000000000000000000000000000000000000000000000000000"
let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count))
var cSessionId: [CChar] = sessionId.cArray.nullTerminated()
var contact: contacts_contact = contacts_contact()
guard contacts_get_or_construct(conf, &contact, &cSessionId) else {
throw SessionUtilError.getOrConstructFailedUnexpectedly
}
// Set the values to the maximum data that can fit
properties.forEach { property in
switch property {
case .approved: contact.approved = true
case .approved_me: contact.approved_me = true
case .blocked: contact.blocked = true
case .created: contact.created = Int64.max
case .notifications: contact.notifications = CONVO_NOTIFY_MENTIONS_ONLY
case .mute_until: contact.mute_until = Int64.max
case .name:
contact.name = String(
data: Data(
repeating: "A".data(using: .utf8)![0],
count: SessionUtil.libSessionMaxNameByteLength
),
encoding: .utf8
).toLibSession()
case .nickname:
contact.nickname = String(
data: Data(
repeating: "A".data(using: .utf8)![0],
count: SessionUtil.libSessionMaxNameByteLength
),
encoding: .utf8
).toLibSession()
case .profile_pic:
contact.profile_pic = user_profile_pic(
url: String(
data: Data(
repeating: "A".data(using: .utf8)![0],
count: SessionUtil.libSessionMaxProfileUrlByteLength
),
encoding: .utf8
).toLibSession(),
key: "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
)
}
}
return contact
}
}
fileprivate extension Array where Element == ConfigContactsSpec.ContactProperty {
static var allProperties: [ConfigContactsSpec.ContactProperty] = ConfigContactsSpec.ContactProperty.allCases
}

View File

@ -11,255 +11,257 @@ import Nimble
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigConvoInfoVolatileSpec {
// MARK: - Spec
static func spec() {
it("generates ConvoInfoVolatile configs correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse())
expect(convo_info_volatile_size(conf)).to(equal(0))
var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId))
.to(beTrue())
expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId))
expect(oneToOne2.last_read).to(equal(0))
expect(oneToOne2.unread).to(beFalse())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// Update the last read
let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
oneToOne2.last_read = nowTimestampMs
// The new data doesn't get stored until we call this:
convo_info_volatile_set_1to1(conf, &oneToOne2)
var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId))
.to(beFalse())
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue())
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
let openGroupBaseUrl: String = "http://Example.ORG:5678"
var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated()
let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased()
// ("http://Example.ORG:5678"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
// )
let openGroupRoom: String = "SudokuRoom"
var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated()
let openGroupRoomResult: String = openGroupRoom.lowercased()
// ("SudokuRoom"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (65 - openGroupRoom.count))
// )
var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.bytes
var community1: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.unread = true
// The new data doesn't get stored until we call this:
convo_info_volatile_set_community(conf, &community1);
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
pushData1.deallocate()
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true))
expect(oneToOne4.last_read).to(equal(nowTimestampMs))
expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId))
expect(oneToOne4.unread).to(beFalse())
var community2: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community2.unread = true
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue())
oneToOne5.unread = true
convo_info_volatile_set_1to1(conf2, &oneToOne5)
let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue())
legacyGroup2.last_read = (nowTimestampMs - 50)
convo_info_volatile_set_legacy_group(conf2, &legacyGroup2)
expect(config_needs_push(conf2)).to(beTrue())
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData2.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData2.pointee.config)]
var mergeSize: [Int] = [pushData2.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
expect(convo_info_volatile_size(conf)).to(equal(4))
expect(convo_info_volatile_size_1to1(conf)).to(equal(2))
expect(convo_info_volatile_size_communities(conf)).to(equal(1))
expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1))
context("CONVO_INFO_VOLATILE") {
it("generates config correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var c2: convo_info_volatile_community = convo_info_volatile_community()
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
while !convo_info_volatile_iterator_done(it) {
if convo_info_volatile_it_is_1to1(it, &c1) {
seen.append("1-to-1: \(String(libSessionVal: c1.session_id))")
}
else if convo_info_volatile_it_is_community(it, &c2) {
seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else if convo_info_volatile_it_is_legacy_group(it, &c3) {
seen.append("cl: \(String(libSessionVal: c3.group_id))")
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse())
expect(convo_info_volatile_size(conf)).to(equal(0))
var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId))
.to(beTrue())
expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId))
expect(oneToOne2.last_read).to(equal(0))
expect(oneToOne2.unread).to(beFalse())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// Update the last read
let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
oneToOne2.last_read = nowTimestampMs
// The new data doesn't get stored until we call this:
convo_info_volatile_set_1to1(conf, &oneToOne2)
var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId))
.to(beFalse())
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue())
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
let openGroupBaseUrl: String = "http://Example.ORG:5678"
var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated()
let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased()
// ("http://Example.ORG:5678"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
// )
let openGroupRoom: String = "SudokuRoom"
var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated()
let openGroupRoomResult: String = openGroupRoom.lowercased()
// ("SudokuRoom"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (65 - openGroupRoom.count))
// )
var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.bytes
var community1: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.unread = true
// The new data doesn't get stored until we call this:
convo_info_volatile_set_community(conf, &community1);
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
pushData1.deallocate()
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true))
expect(oneToOne4.last_read).to(equal(nowTimestampMs))
expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId))
expect(oneToOne4.unread).to(beFalse())
var community2: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community2.unread = true
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue())
oneToOne5.unread = true
convo_info_volatile_set_1to1(conf2, &oneToOne5)
let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue())
legacyGroup2.last_read = (nowTimestampMs - 50)
convo_info_volatile_set_legacy_group(conf2, &legacyGroup2)
expect(config_needs_push(conf2)).to(beTrue())
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData2.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData2.pointee.config)]
var mergeSize: [Int] = [pushData2.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
expect(convo_info_volatile_size(conf)).to(equal(4))
expect(convo_info_volatile_size_1to1(conf)).to(equal(2))
expect(convo_info_volatile_size_communities(conf)).to(equal(1))
expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1))
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var c2: convo_info_volatile_community = convo_info_volatile_community()
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
while !convo_info_volatile_iterator_done(it) {
if convo_info_volatile_it_is_1to1(it, &c1) {
seen.append("1-to-1: \(String(libSessionVal: c1.session_id))")
}
else if convo_info_volatile_it_is_community(it, &c2) {
seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else if convo_info_volatile_it_is_legacy_group(it, &c3) {
seen.append("cl: \(String(libSessionVal: c3.group_id))")
}
convo_info_volatile_iterator_advance(it)
}
convo_info_volatile_iterator_advance(it)
convo_info_volatile_iterator_free(it)
expect(seen).to(equal([
"1-to-1: 051111111111111111111111111111111111111111111111111111111111111111",
"1-to-1: 055000000000000000000000000000000000000000000000000000000000000000",
"og: http://example.org:5678/r/sudokuroom",
"cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
convo_info_volatile_iterator_free(it)
let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000"
var cFourthId: [CChar] = fourthId.cArray.nullTerminated()
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cFourthId)
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId)
expect(config_needs_push(conf)).to(beTrue())
expect(convo_info_volatile_size(conf)).to(equal(3))
expect(convo_info_volatile_size_1to1(conf)).to(equal(1))
expect(seen).to(equal([
"1-to-1: 051111111111111111111111111111111111111111111111111111111111111111",
"1-to-1: 055000000000000000000000000000000000000000000000000000000000000000",
"og: http://example.org:5678/r/sudokuroom",
"cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
// Check the single-type iterators:
var seen1: [String?] = []
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf)
while !convo_info_volatile_iterator_done(it1) {
expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue())
seen1.append(String(libSessionVal: c1.session_id))
convo_info_volatile_iterator_advance(it1)
}
convo_info_volatile_iterator_free(it1)
expect(seen1).to(equal([
"051111111111111111111111111111111111111111111111111111111111111111"
]))
var seen2: [String?] = []
var c2: convo_info_volatile_community = convo_info_volatile_community()
let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf)
while !convo_info_volatile_iterator_done(it2) {
expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue())
seen2.append(String(libSessionVal: c2.base_url))
convo_info_volatile_iterator_advance(it2)
}
convo_info_volatile_iterator_free(it2)
expect(seen2).to(equal([
"http://example.org:5678"
]))
var seen3: [String?] = []
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf)
while !convo_info_volatile_iterator_done(it3) {
expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue())
seen3.append(String(libSessionVal: c3.group_id))
convo_info_volatile_iterator_advance(it3)
}
convo_info_volatile_iterator_free(it3)
expect(seen3).to(equal([
"05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000"
var cFourthId: [CChar] = fourthId.cArray.nullTerminated()
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cFourthId)
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId)
expect(config_needs_push(conf)).to(beTrue())
expect(convo_info_volatile_size(conf)).to(equal(3))
expect(convo_info_volatile_size_1to1(conf)).to(equal(1))
// Check the single-type iterators:
var seen1: [String?] = []
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf)
while !convo_info_volatile_iterator_done(it1) {
expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue())
seen1.append(String(libSessionVal: c1.session_id))
convo_info_volatile_iterator_advance(it1)
}
convo_info_volatile_iterator_free(it1)
expect(seen1).to(equal([
"051111111111111111111111111111111111111111111111111111111111111111"
]))
var seen2: [String?] = []
var c2: convo_info_volatile_community = convo_info_volatile_community()
let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf)
while !convo_info_volatile_iterator_done(it2) {
expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue())
seen2.append(String(libSessionVal: c2.base_url))
convo_info_volatile_iterator_advance(it2)
}
convo_info_volatile_iterator_free(it2)
expect(seen2).to(equal([
"http://example.org:5678"
]))
var seen3: [String?] = []
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf)
while !convo_info_volatile_iterator_done(it3) {
expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue())
seen3.append(String(libSessionVal: c3.group_id))
convo_info_volatile_iterator_advance(it3)
}
convo_info_volatile_iterator_free(it3)
expect(seen3).to(equal([
"05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
}
}

View File

@ -82,505 +82,507 @@ class ConfigUserGroupsSpec {
expect(result8?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
}
it("generates UserGroup configs correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let legacyGroup1: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup1?.pointee).to(beNil())
expect(user_groups_size(conf)).to(equal(0))
let legacyGroup2: UnsafeMutablePointer<ugroups_legacy_group_info> = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup2.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup2.pointee.session_id))
.to(equal(definitelyRealId))
expect(legacyGroup2.pointee.disappearing_timer).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup2.pointee.priority).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal(""))
expect(legacyGroup2.pointee.joined_at).to(equal(0))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(legacyGroup2.pointee.mute_until).to(equal(0))
// Iterate through and make sure we got everything we expected
var membersSeen1: [String: Bool] = [:]
var memberSessionId1: UnsafePointer<CChar>? = nil
var memberAdmin1: Bool = false
let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) {
membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1
}
ugroups_legacy_members_free(membersIt1)
expect(membersSeen1).to(beEmpty())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len))
.to(beEmpty())
expect(pushData1.pointee.config_len).to(equal(256))
pushData1.deallocate()
let users: [String] = [
"050000000000000000000000000000000000000000000000000000000000000000",
"051111111111111111111111111111111111111111111111111111111111111111",
"052222222222222222222222222222222222222222222222222222222222222222",
"053333333333333333333333333333333333333333333333333333333333333333",
"054444444444444444444444444444444444444444444444444444444444444444",
"055555555555555555555555555555555555555555555555555555555555555555",
"056666666666666666666666666666666666666666666666666666666666666666"
]
var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() }
legacyGroup2.pointee.name = "Englishmen".toLibSession()
legacyGroup2.pointee.disappearing_timer = 60
legacyGroup2.pointee.joined_at = createdTs
legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL
legacyGroup2.pointee.mute_until = (nowTs + 3600)
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse())
// Flip to and from admin
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue())
var membersSeen2: [String: Bool] = [:]
var memberSessionId2: UnsafePointer<CChar>? = nil
var memberAdmin2: Bool = false
let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) {
membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2
}
ugroups_legacy_members_free(membersIt2)
expect(membersSeen2).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)!
let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)!
// Note: this isn't exactly what Session actually does here for legacy closed
// groups (rather it uses X25519 keys) but for this test the distinction doesn't matter.
legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession()
legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession()
legacyGroup2.pointee.priority = 3
expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString())
.to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"))
expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString())
.to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"))
// The new data doesn't get stored until we call this:
user_groups_set_free_legacy_group(conf, legacyGroup2)
let legacyGroup3: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup3?.pointee).toNot(beNil())
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
ugroups_legacy_group_free(legacyGroup3)
let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated()
var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community1: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey))
.to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case
expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.priority = 14
// The new data doesn't get stored until we call this:
user_groups_set_community(conf, &community1)
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len))
.to(beEmpty())
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2
expect(config_needs_push(conf)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(1))
expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len))
.to(beEmpty())
pushData3.deallocate()
let currentHashes1: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf)
expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len))
.to(beEmpty())
pushData4.deallocate()
let currentHashes2: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes2?.deallocate()
expect(user_groups_size(conf2)).to(equal(2))
expect(user_groups_size_communities(conf2)).to(equal(1))
expect(user_groups_size_legacy_groups(conf2)).to(equal(1))
let legacyGroup4: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(legacyGroup4?.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60))
expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId))
expect(legacyGroup4?.pointee.priority).to(equal(3))
expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen"))
expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600))
var membersSeen3: [String: Bool] = [:]
var memberSessionId3: UnsafePointer<CChar>? = nil
var memberAdmin3: Bool = false
let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4)
while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) {
membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3
}
ugroups_legacy_members_free(membersIt3)
ugroups_legacy_group_free(legacyGroup4)
expect(membersSeen3).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData5.deallocate()
for targetConf in [conf, conf2] {
context("USER_GROUPS") {
it("generates config correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let legacyGroup1: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup1?.pointee).to(beNil())
expect(user_groups_size(conf)).to(equal(0))
let legacyGroup2: UnsafeMutablePointer<ugroups_legacy_group_info> = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup2.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup2.pointee.session_id))
.to(equal(definitelyRealId))
expect(legacyGroup2.pointee.disappearing_timer).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup2.pointee.priority).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal(""))
expect(legacyGroup2.pointee.joined_at).to(equal(0))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(legacyGroup2.pointee.mute_until).to(equal(0))
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var membersSeen1: [String: Bool] = [:]
var memberSessionId1: UnsafePointer<CChar>? = nil
var memberAdmin1: Bool = false
let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) {
membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1
}
user_groups_iterator_free(it)
ugroups_legacy_members_free(membersIt1)
expect(seen).to(equal([
"community: http://example.org:5678/r/SudokuRoom",
"legacy: Englishmen, 1 admins, 2 members"
expect(membersSeen1).to(beEmpty())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len))
.to(beEmpty())
expect(pushData1.pointee.config_len).to(equal(256))
pushData1.deallocate()
let users: [String] = [
"050000000000000000000000000000000000000000000000000000000000000000",
"051111111111111111111111111111111111111111111111111111111111111111",
"052222222222222222222222222222222222222222222222222222222222222222",
"053333333333333333333333333333333333333333333333333333333333333333",
"054444444444444444444444444444444444444444444444444444444444444444",
"055555555555555555555555555555555555555555555555555555555555555555",
"056666666666666666666666666666666666666666666666666666666666666666"
]
var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() }
legacyGroup2.pointee.name = "Englishmen".toLibSession()
legacyGroup2.pointee.disappearing_timer = 60
legacyGroup2.pointee.joined_at = createdTs
legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL
legacyGroup2.pointee.mute_until = (nowTs + 3600)
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse())
// Flip to and from admin
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue())
var membersSeen2: [String: Bool] = [:]
var memberSessionId2: UnsafePointer<CChar>? = nil
var memberAdmin2: Bool = false
let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) {
membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2
}
ugroups_legacy_members_free(membersIt2)
expect(membersSeen2).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
}
var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated()
var community2: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room))
.to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678"))
expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(community2.priority).to(equal(14))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData6.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData6.deallocate()
community2.room = "sudokuRoom".toLibSession() // Change capitalization
user_groups_set_community(conf2, &community2)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2)
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash1]))
let currentHashes3: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len))
.to(equal([fakeHash2]))
currentHashes3?.deallocate()
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf2, &dump2, &dump2Len)
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData8.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2)
expect(config_needs_dump(conf2)).to(beFalse())
var mergeHashes1: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData1: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData8.pointee.config)]
var mergeSize1: [Int] = [pushData8.pointee.config_len]
expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1))
pushData8.deallocate()
var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community3: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room))
.to(beTrue())
expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change
expect(user_groups_size(conf)).to(equal(2))
expect(user_groups_size_communities(conf)).to(equal(1))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let legacyGroup5: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(2))
expect(config_needs_dump(conf2)).to(beFalse())
pushData9.deallocate()
user_groups_set_free_legacy_group(conf2, legacyGroup5)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated()
var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated()
user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData10: UnsafeMutablePointer<config_push_data> = config_push(conf2)
config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3)
expect(pushData10.pointee.seqno).to(equal(3))
expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len))
.to(equal([fakeHash2]))
let currentHashes4: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len))
.to(equal([fakeHash3]))
currentHashes4?.deallocate()
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData10.pointee.config)]
var mergeSize2: [Int] = [pushData10.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(user_groups_size(conf)).to(equal(1))
expect(user_groups_size_communities(conf)).to(equal(0))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
var prio: Int32 = 0
var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated()
var cBeanstalkPubkey: [UInt8] = Data(
hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
).cArray
["fee", "fi", "fo", "fum"].forEach { room in
var cRoom: [CChar] = room.cArray.nullTerminated()
prio += 1
var community4: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey))
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)!
let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)!
// Note: this isn't exactly what Session actually does here for legacy closed
// groups (rather it uses X25519 keys) but for this test the distinction doesn't matter.
legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession()
legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession()
legacyGroup2.pointee.priority = 3
expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString())
.to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"))
expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString())
.to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"))
// The new data doesn't get stored until we call this:
user_groups_set_free_legacy_group(conf, legacyGroup2)
let legacyGroup3: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup3?.pointee).toNot(beNil())
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
ugroups_legacy_group_free(legacyGroup3)
let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated()
var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community1: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey))
.to(beTrue())
community4.priority = prio
user_groups_set_community(conf, &community4)
}
expect(user_groups_size(conf)).to(equal(5))
expect(user_groups_size_communities(conf)).to(equal(4))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let pushData11: UnsafeMutablePointer<config_push_data> = config_push(conf)
config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4)
expect(pushData11.pointee.seqno).to(equal(4))
expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len))
.to(equal([fakeHash3, fakeHash2, fakeHash1]))
// Load some obsolete ones in just to check that they get immediately obsoleted
let fakeHash10: String = "fakehash10"
let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated()
let fakeHash11: String = "fakehash11"
let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated()
let fakeHash12: String = "fakehash12"
let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [
UnsafePointer(pushData10.pointee.config),
UnsafePointer(pushData2.pointee.config),
UnsafePointer(pushData7.pointee.config),
UnsafePointer(pushData11.pointee.config)
]
var mergeSize3: [Int] = [
pushData10.pointee.config_len,
pushData2.pointee.config_len,
pushData7.pointee.config_len,
pushData11.pointee.config_len
]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4))
expect(config_needs_dump(conf2)).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
pushData2.deallocate()
pushData7.deallocate()
pushData10.deallocate()
pushData11.deallocate()
let currentHashes5: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len))
.to(equal([fakeHash4]))
currentHashes5?.deallocate()
let pushData12: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData12.pointee.seqno).to(equal(4))
expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len))
.to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3]))
pushData12.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case
expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.priority = 14
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
// The new data doesn't get stored until we call this:
user_groups_set_community(conf, &community1)
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len))
.to(beEmpty())
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2
expect(config_needs_push(conf)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(1))
expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len))
.to(beEmpty())
pushData3.deallocate()
let currentHashes1: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf)
expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len))
.to(beEmpty())
pushData4.deallocate()
let currentHashes2: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes2?.deallocate()
expect(user_groups_size(conf2)).to(equal(2))
expect(user_groups_size_communities(conf2)).to(equal(1))
expect(user_groups_size_legacy_groups(conf2)).to(equal(1))
let legacyGroup4: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(legacyGroup4?.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60))
expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId))
expect(legacyGroup4?.pointee.priority).to(equal(3))
expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen"))
expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600))
var membersSeen3: [String: Bool] = [:]
var memberSessionId3: UnsafePointer<CChar>? = nil
var memberAdmin3: Bool = false
let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4)
while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) {
membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3
}
user_groups_iterator_free(it)
ugroups_legacy_members_free(membersIt3)
ugroups_legacy_group_free(legacyGroup4)
expect(seen).to(equal([
"community: http://jacksbeanstalk.org/r/fee",
"community: http://jacksbeanstalk.org/r/fi",
"community: http://jacksbeanstalk.org/r/fo",
"community: http://jacksbeanstalk.org/r/fum",
"legacy: Englishmen, 3 admins, 2 members"
expect(membersSeen3).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData5.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
}
user_groups_iterator_free(it)
expect(seen).to(equal([
"community: http://example.org:5678/r/SudokuRoom",
"legacy: Englishmen, 1 admins, 2 members"
]))
}
var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated()
var community2: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room))
.to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678"))
expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(community2.priority).to(equal(14))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData6.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData6.deallocate()
community2.room = "sudokuRoom".toLibSession() // Change capitalization
user_groups_set_community(conf2, &community2)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2)
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash1]))
let currentHashes3: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len))
.to(equal([fakeHash2]))
currentHashes3?.deallocate()
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf2, &dump2, &dump2Len)
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData8.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2)
expect(config_needs_dump(conf2)).to(beFalse())
var mergeHashes1: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData1: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData8.pointee.config)]
var mergeSize1: [Int] = [pushData8.pointee.config_len]
expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1))
pushData8.deallocate()
var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community3: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room))
.to(beTrue())
expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change
expect(user_groups_size(conf)).to(equal(2))
expect(user_groups_size_communities(conf)).to(equal(1))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let legacyGroup5: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(2))
expect(config_needs_dump(conf2)).to(beFalse())
pushData9.deallocate()
user_groups_set_free_legacy_group(conf2, legacyGroup5)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated()
var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated()
user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData10: UnsafeMutablePointer<config_push_data> = config_push(conf2)
config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3)
expect(pushData10.pointee.seqno).to(equal(3))
expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len))
.to(equal([fakeHash2]))
let currentHashes4: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len))
.to(equal([fakeHash3]))
currentHashes4?.deallocate()
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData10.pointee.config)]
var mergeSize2: [Int] = [pushData10.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(user_groups_size(conf)).to(equal(1))
expect(user_groups_size_communities(conf)).to(equal(0))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
var prio: Int32 = 0
var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated()
var cBeanstalkPubkey: [UInt8] = Data(
hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
).cArray
["fee", "fi", "fo", "fum"].forEach { room in
var cRoom: [CChar] = room.cArray.nullTerminated()
prio += 1
var community4: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey))
.to(beTrue())
community4.priority = prio
user_groups_set_community(conf, &community4)
}
expect(user_groups_size(conf)).to(equal(5))
expect(user_groups_size_communities(conf)).to(equal(4))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let pushData11: UnsafeMutablePointer<config_push_data> = config_push(conf)
config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4)
expect(pushData11.pointee.seqno).to(equal(4))
expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len))
.to(equal([fakeHash3, fakeHash2, fakeHash1]))
// Load some obsolete ones in just to check that they get immediately obsoleted
let fakeHash10: String = "fakehash10"
let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated()
let fakeHash11: String = "fakehash11"
let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated()
let fakeHash12: String = "fakehash12"
let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [
UnsafePointer(pushData10.pointee.config),
UnsafePointer(pushData2.pointee.config),
UnsafePointer(pushData7.pointee.config),
UnsafePointer(pushData11.pointee.config)
]
var mergeSize3: [Int] = [
pushData10.pointee.config_len,
pushData2.pointee.config_len,
pushData7.pointee.config_len,
pushData11.pointee.config_len
]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4))
expect(config_needs_dump(conf2)).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
pushData2.deallocate()
pushData7.deallocate()
pushData10.deallocate()
pushData11.deallocate()
let currentHashes5: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len))
.to(equal([fakeHash4]))
currentHashes5?.deallocate()
let pushData12: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData12.pointee.seqno).to(equal(4))
expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len))
.to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3]))
pushData12.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
}
user_groups_iterator_free(it)
expect(seen).to(equal([
"community: http://jacksbeanstalk.org/r/fee",
"community: http://jacksbeanstalk.org/r/fi",
"community: http://jacksbeanstalk.org/r/fo",
"community: http://jacksbeanstalk.org/r/fum",
"legacy: Englishmen, 3 admins, 2 members"
]))
}
}
}
}

View File

@ -12,386 +12,388 @@ import Nimble
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigUserProfileSpec {
// MARK: - Spec
static func spec() {
it("generates UserProfile configs correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// We don't need to push anything, since this is an empty config
expect(config_needs_push(conf)).to(beFalse())
// And we haven't changed anything so don't need to dump to db
expect(config_needs_dump(conf)).to(beFalse())
// Since it's empty there shouldn't be a name.
let namePtr: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr).to(beNil())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee).toNot(beNil())
expect(pushData1.pointee.seqno).to(equal(0))
expect(pushData1.pointee.config_len).to(equal(256))
let encDomain: [CChar] = "UserProfile"
.bytes
.map { CChar(bitPattern: $0) }
expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile"))
var toPushDecSize: Int = 0
let toPushDecrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize)
let prefixPadding: String = (0..<193)
.map { _ in "\0" }
.joined()
expect(toPushDecrypted).toNot(beNil())
expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead
expect(String(pointer: toPushDecrypted, length: toPushDecSize))
.to(equal("\(prefixPadding)d1:#i0e1:&de1:<le1:=dee"))
pushData1.deallocate()
toPushDecrypted?.deallocate()
// This should also be unset:
let pic: user_profile_pic = user_profile_get_pic(conf)
expect(String(libSessionVal: pic.url)).to(beEmpty())
// Now let's go set a profile name and picture:
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
let p: user_profile_pic = user_profile_pic(
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
)
expect(user_profile_set_pic(conf, p)).to(equal(0))
user_profile_set_nts_priority(conf, 9)
// Retrieve them just to make sure they set properly:
let namePtr2: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr2).toNot(beNil())
expect(String(cString: namePtr2!)).to(equal("Kallie"))
let pic2: user_profile_pic = user_profile_get_pic(conf);
expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp"))
expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength))
.to(equal("secret78901234567890123456789012".data(using: .utf8)))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
// Note: This hex value differs from the value in the library tests because
// it looks like the library has an "end of cell mark" character added at the
// end (0x07 or '0007') so we need to manually add it to work
let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
.bytes
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
let expPush1Decrypted: [UInt8] = ["""
d
1:#i1e
1:& d
1:+ i9e
1:n 6:Kallie
1:p 34:http://example.org/omg-pic-123.bmp
1:q 32:secret78901234567890123456789012
e
1:< l
l i0e 32:
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes,
expHash0,
"""
de e
e
1:= d
1:+ 0:
1:n 0:
1:p 0:
1:q 0:
e
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
].flatMap { $0 }
let expPush1Encrypted: [UInt8] = Data(hex: [
"9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f",
"1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84",
"aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868",
"2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d",
"e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656",
"056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea",
"49bf122762d7bc1d6d9c02f6d54f8384"
].joined()).bytes
let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)!
let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)!
expect(pushData2Str).to(equal(expPush1EncryptedStr))
// Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data)
var pushData2DecSize: Int = 0
let pushData2Decrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(
pushData2.pointee.config,
pushData2.pointee.config_len,
edSK,
encDomain,
&pushData2DecSize
)
let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count))
.map { _ in "\0" }
.joined()
expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead
let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)!
let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii)
.map { "\(prefixPadding2)\($0)" }!
expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr))
pushData2Decrypted?.deallocate()
// We haven't dumped, so still need to dump:
expect(config_needs_dump(conf)).to(beTrue())
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
expect(config_needs_push(conf)).to(beTrue())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
// (in a real client we'd now store this to disk)
expect(config_needs_dump(conf)).to(beFalse())
let expDump1: [CChar] = [
"""
d
1:! i2e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(0:
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
context("USER_PROFILE") {
it("generates config correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// We don't need to push anything, since this is an empty config
expect(config_needs_push(conf)).to(beFalse())
// And we haven't changed anything so don't need to dump to db
expect(config_needs_dump(conf)).to(beFalse())
// Since it's empty there shouldn't be a name.
let namePtr: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr).to(beNil())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee).toNot(beNil())
expect(pushData1.pointee.seqno).to(equal(0))
expect(pushData1.pointee.config_len).to(equal(256))
let encDomain: [CChar] = "UserProfile"
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump1, length: dump1Len, encoding: .ascii))
.to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii)))
dump1?.deallocate()
// So now imagine we got back confirmation from the swarm that the push has been stored:
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf, &dump2, &dump2Len)
let expDump2: [CChar] = [
"""
expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile"))
var toPushDecSize: Int = 0
let toPushDecrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize)
let prefixPadding: String = (0..<193)
.map { _ in "\0" }
.joined()
expect(toPushDecrypted).toNot(beNil())
expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead
expect(String(pointer: toPushDecrypted, length: toPushDecSize))
.to(equal("\(prefixPadding)d1:#i0e1:&de1:<le1:=dee"))
pushData1.deallocate()
toPushDecrypted?.deallocate()
// This should also be unset:
let pic: user_profile_pic = user_profile_get_pic(conf)
expect(String(libSessionVal: pic.url)).to(beEmpty())
// Now let's go set a profile name and picture:
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
let p: user_profile_pic = user_profile_pic(
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
)
expect(user_profile_set_pic(conf, p)).to(equal(0))
user_profile_set_nts_priority(conf, 9)
// Retrieve them just to make sure they set properly:
let namePtr2: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr2).toNot(beNil())
expect(String(cString: namePtr2!)).to(equal("Kallie"))
let pic2: user_profile_pic = user_profile_get_pic(conf);
expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp"))
expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength))
.to(equal("secret78901234567890123456789012".data(using: .utf8)))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
// Note: This hex value differs from the value in the library tests because
// it looks like the library has an "end of cell mark" character added at the
// end (0x07 or '0007') so we need to manually add it to work
let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
.bytes
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
let expPush1Decrypted: [UInt8] = ["""
d
1:! i0e
1:$ \(expPush1Decrypted.count):
1:#i1e
1:& d
1:+ i9e
1:n 6:Kallie
1:p 34:http://example.org/omg-pic-123.bmp
1:q 32:secret78901234567890123456789012
e
1:< l
l i0e 32:
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes,
expHash0,
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(9:fakehash1
1:)le
de e
e
1:= d
1:+ 0:
1:n 0:
1:p 0:
1:q 0:
e
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump2, length: dump2Len, encoding: .ascii))
.to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii)))
dump2?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
// Now we're going to set up a second, competing config object (in the real world this would be
// another Session client somewhere).
// Start with an empty config, as above:
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0))
expect(config_needs_dump(conf2)).to(beFalse())
error2?.deallocate()
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
// conf2:
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash1].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [expPush1Encrypted].unsafeCopy()
var mergeSize: [Int] = [expPush1Encrypted.count]
expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
mergeHashes.forEach { $0?.deallocate() }
mergeData.forEach { $0?.deallocate() }
// Our state has changed, so we need to dump:
expect(config_needs_dump(conf2)).to(beTrue())
var dump3: UnsafeMutablePointer<UInt8>? = nil
var dump3Len: Int = 0
config_dump(conf2, &dump3, &dump3Len)
// (store in db)
dump3?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
// We *don't* need to push: even though we updated, all we did is update to the merged data (and
// didn't have any sort of merge conflict needed):
expect(config_needs_push(conf2)).to(beFalse())
// Now let's create a conflicting update:
// Change the name on both clients:
user_profile_set_name(conf, "Nibbler")
user_profile_set_name(conf2, "Raz")
// And, on conf2, we're also going to change the profile pic:
let p2: user_profile_pic = user_profile_pic(
url: "http://new.example.com/pic".toLibSession(),
key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession()
)
user_profile_set_pic(conf2, p2)
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3)
var dump4: UnsafeMutablePointer<UInt8>? = nil
var dump4Len: Int = 0
config_dump(conf, &dump4, &dump4Len);
var dump5: UnsafeMutablePointer<UInt8>? = nil
var dump5Len: Int = 0
config_dump(conf2, &dump5, &dump5Len);
// (store in db)
dump4?.deallocate()
dump5?.deallocate()
// Since we set different things, we're going to get back different serialized data to be
// pushed:
let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii)
let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii)
expect(pushData3Str).toNot(equal(pushData4Str))
// Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client
// also fetches new messages and pulls down the other client's `seqno=2` value.
// Feed the new config into each other. (This array could hold multiple configs if we pulled
// down more than one).
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData3.pointee.config)]
var mergeSize2: [Int] = [pushData3.pointee.config_len]
expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
pushData3.deallocate()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize3: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
pushData4.deallocate()
// Now after the merge we *will* want to push from both client, since both will have generated a
// merge conflict update (with seqno = 3).
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(3))
expect(pushData6.pointee.seqno).to(equal(3))
// They should have resolved the conflict to the same thing:
expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler"))
expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler"))
// (Note that they could have also both resolved to "Raz" here, but the hash of the serialized
// message just happens to have a higher hash -- and thus gets priority -- for this particular
// test).
// Since only one of them set a profile pic there should be no conflict there:
let pic3: user_profile_pic = user_profile_get_pic(conf)
expect(pic3.url).toNot(beNil())
expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic"))
expect(pic3.key).toNot(beNil())
expect(Data(libSessionVal: pic3.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
let pic4: user_profile_pic = user_profile_get_pic(conf2)
expect(pic4.url).toNot(beNil())
expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic"))
expect(pic4.key).toNot(beNil())
expect(Data(libSessionVal: pic4.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
expect(user_profile_get_nts_priority(conf2)).to(equal(9))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let fakeHash5: String = "fakehash5"
var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated()
config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5)
pushData5.deallocate()
pushData6.deallocate()
var dump6: UnsafeMutablePointer<UInt8>? = nil
var dump6Len: Int = 0
config_dump(conf, &dump6, &dump6Len);
var dump7: UnsafeMutablePointer<UInt8>? = nil
var dump7Len: Int = 0
config_dump(conf2, &dump7, &dump7Len);
// (store in db)
dump6?.deallocate()
dump7?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Wouldn't do this in a normal session but doing it here to properly clean up
// after the test
conf?.deallocate()
conf2?.deallocate()
].flatMap { $0 }
let expPush1Encrypted: [UInt8] = Data(hex: [
"9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f",
"1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84",
"aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868",
"2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d",
"e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656",
"056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea",
"49bf122762d7bc1d6d9c02f6d54f8384"
].joined()).bytes
let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)!
let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)!
expect(pushData2Str).to(equal(expPush1EncryptedStr))
// Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data)
var pushData2DecSize: Int = 0
let pushData2Decrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(
pushData2.pointee.config,
pushData2.pointee.config_len,
edSK,
encDomain,
&pushData2DecSize
)
let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count))
.map { _ in "\0" }
.joined()
expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead
let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)!
let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii)
.map { "\(prefixPadding2)\($0)" }!
expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr))
pushData2Decrypted?.deallocate()
// We haven't dumped, so still need to dump:
expect(config_needs_dump(conf)).to(beTrue())
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
expect(config_needs_push(conf)).to(beTrue())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
// (in a real client we'd now store this to disk)
expect(config_needs_dump(conf)).to(beFalse())
let expDump1: [CChar] = [
"""
d
1:! i2e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(0:
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump1, length: dump1Len, encoding: .ascii))
.to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii)))
dump1?.deallocate()
// So now imagine we got back confirmation from the swarm that the push has been stored:
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf, &dump2, &dump2Len)
let expDump2: [CChar] = [
"""
d
1:! i0e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(9:fakehash1
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump2, length: dump2Len, encoding: .ascii))
.to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii)))
dump2?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
// Now we're going to set up a second, competing config object (in the real world this would be
// another Session client somewhere).
// Start with an empty config, as above:
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0))
expect(config_needs_dump(conf2)).to(beFalse())
error2?.deallocate()
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
// conf2:
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash1].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [expPush1Encrypted].unsafeCopy()
var mergeSize: [Int] = [expPush1Encrypted.count]
expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
mergeHashes.forEach { $0?.deallocate() }
mergeData.forEach { $0?.deallocate() }
// Our state has changed, so we need to dump:
expect(config_needs_dump(conf2)).to(beTrue())
var dump3: UnsafeMutablePointer<UInt8>? = nil
var dump3Len: Int = 0
config_dump(conf2, &dump3, &dump3Len)
// (store in db)
dump3?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
// We *don't* need to push: even though we updated, all we did is update to the merged data (and
// didn't have any sort of merge conflict needed):
expect(config_needs_push(conf2)).to(beFalse())
// Now let's create a conflicting update:
// Change the name on both clients:
user_profile_set_name(conf, "Nibbler")
user_profile_set_name(conf2, "Raz")
// And, on conf2, we're also going to change the profile pic:
let p2: user_profile_pic = user_profile_pic(
url: "http://new.example.com/pic".toLibSession(),
key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession()
)
user_profile_set_pic(conf2, p2)
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3)
var dump4: UnsafeMutablePointer<UInt8>? = nil
var dump4Len: Int = 0
config_dump(conf, &dump4, &dump4Len);
var dump5: UnsafeMutablePointer<UInt8>? = nil
var dump5Len: Int = 0
config_dump(conf2, &dump5, &dump5Len);
// (store in db)
dump4?.deallocate()
dump5?.deallocate()
// Since we set different things, we're going to get back different serialized data to be
// pushed:
let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii)
let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii)
expect(pushData3Str).toNot(equal(pushData4Str))
// Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client
// also fetches new messages and pulls down the other client's `seqno=2` value.
// Feed the new config into each other. (This array could hold multiple configs if we pulled
// down more than one).
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData3.pointee.config)]
var mergeSize2: [Int] = [pushData3.pointee.config_len]
expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
pushData3.deallocate()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize3: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
pushData4.deallocate()
// Now after the merge we *will* want to push from both client, since both will have generated a
// merge conflict update (with seqno = 3).
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(3))
expect(pushData6.pointee.seqno).to(equal(3))
// They should have resolved the conflict to the same thing:
expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler"))
expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler"))
// (Note that they could have also both resolved to "Raz" here, but the hash of the serialized
// message just happens to have a higher hash -- and thus gets priority -- for this particular
// test).
// Since only one of them set a profile pic there should be no conflict there:
let pic3: user_profile_pic = user_profile_get_pic(conf)
expect(pic3.url).toNot(beNil())
expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic"))
expect(pic3.key).toNot(beNil())
expect(Data(libSessionVal: pic3.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
let pic4: user_profile_pic = user_profile_get_pic(conf2)
expect(pic4.url).toNot(beNil())
expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic"))
expect(pic4.key).toNot(beNil())
expect(Data(libSessionVal: pic4.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
expect(user_profile_get_nts_priority(conf2)).to(equal(9))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let fakeHash5: String = "fakehash5"
var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated()
config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5)
pushData5.deallocate()
pushData6.deallocate()
var dump6: UnsafeMutablePointer<UInt8>? = nil
var dump6Len: Int = 0
config_dump(conf, &dump6, &dump6Len);
var dump7: UnsafeMutablePointer<UInt8>? = nil
var dump7Len: Int = 0
config_dump(conf2, &dump7, &dump7Len);
// (store in db)
dump6?.deallocate()
dump7?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Wouldn't do this in a normal session but doing it here to properly clean up
// after the test
conf?.deallocate()
conf2?.deallocate()
}
}
}
}

View File

@ -12,9 +12,11 @@ class LibSessionSpec: QuickSpec {
// MARK: - Spec
override func spec() {
ConfigContactsSpec.spec()
ConfigUserProfileSpec.spec()
ConfigConvoInfoVolatileSpec.spec()
ConfigUserGroupsSpec.spec()
describe("libSession") {
ConfigContactsSpec.spec()
ConfigUserProfileSpec.spec()
ConfigConvoInfoVolatileSpec.spec()
ConfigUserGroupsSpec.spec()
}
}
}

View File

@ -53,12 +53,16 @@ class ThreadSettingsViewModelSpec: QuickSpec {
try Profile(
id: "05\(TestConstants.publicKey)",
name: "TestMe"
name: "TestMe",
lastNameUpdate: 0,
lastProfilePictureUpdate: 0
).insert(db)
try Profile(
id: "TestId",
name: "TestUser"
name: "TestUser",
lastNameUpdate: 0,
lastProfilePictureUpdate: 0
).insert(db)
}
viewModel = ThreadSettingsViewModel(

View File

@ -4,15 +4,38 @@ import UIKit
import SessionUtilitiesKit
// FIXME: Refactor as part of the Groups Rebuild
public class ConfirmationModal: Modal {
public class ConfirmationModal: Modal, UITextFieldDelegate {
private static let closeSize: CGFloat = 24
private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil
private var internalOnCancel: ((ConfirmationModal) -> ())? = nil
private var internalOnBodyTap: (() -> ())? = nil
private var internalOnTextChanged: ((String) -> ())? = nil
// MARK: - Components
private lazy var contentTapGestureRecognizer: UITapGestureRecognizer = {
let result: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(contentViewTapped)
)
contentView.addGestureRecognizer(result)
result.isEnabled = false
return result
}()
private lazy var imageViewTapGestureRecognizer: UITapGestureRecognizer = {
let result: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(imageViewTapped)
)
imageViewContainer.addGestureRecognizer(result)
result.isEnabled = false
return result
}()
private lazy var titleLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
@ -36,16 +59,30 @@ public class ConfirmationModal: Modal {
return result
}()
private lazy var textFieldContainer: UIView = {
let result: UIView = UIView()
result.themeBorderColor = .borderSeparator
result.layer.cornerRadius = 11
result.layer.borderWidth = 1
result.isHidden = true
result.set(.height, to: 40)
return result
}()
private lazy var textField: UITextField = {
let result: UITextField = UITextField()
result.font = .systemFont(ofSize: Values.smallFontSize)
result.themeTextColor = .textPrimary
result.delegate = self
return result
}()
private lazy var imageViewContainer: UIView = {
let result: UIView = UIView()
result.isHidden = true
let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(imageViewTapped)
)
result.addGestureRecognizer(gestureRecogniser)
return result
}()
@ -70,7 +107,7 @@ public class ConfirmationModal: Modal {
}()
private lazy var contentStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, imageViewContainer ])
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, textFieldContainer, imageViewContainer ])
result.axis = .vertical
result.spacing = Values.smallSpacing
result.isLayoutMarginsRelativeArrangement = true
@ -132,13 +169,22 @@ public class ConfirmationModal: Modal {
}
public override func populateContentView() {
let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(contentViewTapped)
)
contentView.addGestureRecognizer(gestureRecogniser)
contentView.addSubview(mainStackView)
contentView.addSubview(closeButton)
textFieldContainer.addSubview(textField)
textField.pin(to: textFieldContainer, withInset: 12)
imageViewContainer.addSubview(profileView)
profileView.center(.horizontal, in: imageViewContainer)
profileView.pin(.top, to: .top, of: imageViewContainer)//, withInset: 15)
profileView.pin(.bottom, to: .bottom, of: imageViewContainer)//, withInset: -15)
profileView.pin(.top, to: .top, of: imageViewContainer)
profileView.pin(.bottom, to: .bottom, of: imageViewContainer)
mainStackView.pin(to: contentView)
closeButton.pin(.top, to: .top, of: contentView, withInset: 8)
@ -149,6 +195,7 @@ public class ConfirmationModal: Modal {
public func updateContent(with info: Info) {
internalOnBodyTap = nil
internalOnTextChanged = nil
internalOnConfirm = { modal in
if info.dismissOnConfirm {
modal.close()
@ -161,6 +208,8 @@ public class ConfirmationModal: Modal {
info.onCancel?(modal)
}
contentTapGestureRecognizer.isEnabled = true
imageViewTapGestureRecognizer.isEnabled = false
// Set the content based on the provided info
titleLabel.text = info.title
@ -179,6 +228,15 @@ public class ConfirmationModal: Modal {
explanationLabel.attributedText = attributedText
explanationLabel.isHidden = false
case .input(let explanation, let placeholder, let value, let clearButton, let onTextChanged):
explanationLabel.attributedText = explanation
explanationLabel.isHidden = (explanation == nil)
textField.placeholder = placeholder
textField.text = (value ?? "")
textField.clearButtonMode = (clearButton ? .always : .never)
textFieldContainer.isHidden = false
internalOnTextChanged = onTextChanged
case .image(let placeholder, let value, let icon, let style, let accessibility, let onClick):
imageViewContainer.isAccessibilityElement = (accessibility != nil)
imageViewContainer.accessibilityIdentifier = accessibility?.identifier
@ -193,6 +251,8 @@ public class ConfirmationModal: Modal {
)
)
internalOnBodyTap = onClick
contentTapGestureRecognizer.isEnabled = false
imageViewTapGestureRecognizer.isEnabled = true
}
confirmButton.accessibilityLabel = info.confirmAccessibility?.label
@ -216,8 +276,38 @@ public class ConfirmationModal: Modal {
contentView.accessibilityIdentifier = info.accessibility?.identifier
}
// MARK: - UITextFieldDelegate
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
public func textFieldShouldClear(_ textField: UITextField) -> Bool {
internalOnTextChanged?("")
return true
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text: String = textField.text, let textRange: Range = Range(range, in: text) {
let updatedText = text.replacingCharacters(in: textRange, with: string)
internalOnTextChanged?(updatedText)
}
return true
}
// MARK: - Interaction
@objc private func contentViewTapped() {
if textField.isFirstResponder {
textField.resignFirstResponder()
}
internalOnBodyTap?()
}
@objc private func imageViewTapped() {
internalOnBodyTap?()
}
@ -400,8 +490,14 @@ public extension ConfirmationModal.Info {
case none
case text(String)
case attributedText(NSAttributedString)
// FIXME: Implement these
// case input(placeholder: String, value: String?)
case input(
explanation: NSAttributedString?,
placeholder: String,
initialValue: String?,
clearButton: Bool,
onChange: (String) -> ()
)
// FIXME: Implement this
// case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)])
case image(
placeholderData: Data?,
@ -418,14 +514,15 @@ public extension ConfirmationModal.Info {
case (.text(let lhsText), .text(let rhsText)): return (lhsText == rhsText)
case (.attributedText(let lhsText), .attributedText(let rhsText)): return (lhsText == rhsText)
// FIXME: Implement these
//case (.input(let lhsPlaceholder, let lhsValue), .input(let rhsPlaceholder, let rhsValue)):
// return (
// lhsPlaceholder == rhsPlaceholder &&
// lhsValue == rhsValue &&
// )
case (.input(let lhsExplanation, let lhsPlaceholder, let lhsInitialValue, let lhsClearButton, _), .input(let rhsExplanation, let rhsPlaceholder, let rhsInitialValue, let rhsClearButton, _)):
return (
lhsExplanation == rhsExplanation &&
lhsPlaceholder == rhsPlaceholder &&
lhsInitialValue == rhsInitialValue &&
lhsClearButton == rhsClearButton
)
// FIXME: Implement these
// FIXME: Implement this
//case (.radio(let lhsExplanation, let lhsOptions), .radio(let rhsExplanation, let rhsOptions)):
// return (
// lhsExplanation == rhsExplanation &&
@ -450,6 +547,12 @@ public extension ConfirmationModal.Info {
case .none: break
case .text(let text): text.hash(into: &hasher)
case .attributedText(let text): text.hash(into: &hasher)
case .input(let explanation, let placeholder, let initialValue, let clearButton, _):
explanation.hash(into: &hasher)
placeholder.hash(into: &hasher)
initialValue.hash(into: &hasher)
clearButton.hash(into: &hasher)
case .image(let placeholder, let value, let icon, let style, let accessibility, _):
placeholder.hash(into: &hasher)

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CryptoKit
import Combine
import GRDB
import SignalCoreKit
@ -492,3 +493,38 @@ public extension ValueObservation {
.eraseToAnyPublisher()
}
}
// MARK: - Debug Convenience
#if DEBUG
public extension Storage {
func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) {
var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
guard var passwordData: Data = password.data(using: .utf8) else { throw StorageError.generic }
defer { passwordData.resetBytes(in: 0..<passwordData.count) } // Reset content immediately after use
/// Encrypt the `keySpec` value using a SHA256 of the password provided and a nonce then base64-encode the encrypted
/// data and save it to a temporary file to share alongside the database
///
/// Decrypt the key via the termincal on macOS by running the command in the project root directory
/// `swift ./Scropts/DecryptExportedKey.swift {BASE64_CIPHERTEXT} {PASSWORD}`
///
/// Where `BASE64_CIPHERTEXT` is the content of the `key.enc` file and `PASSWORD` is the password provided via the
/// prompt during export
let nonce: ChaChaPoly.Nonce = ChaChaPoly.Nonce()
let hash: SHA256.Digest = SHA256.hash(data: passwordData)
let key: SymmetricKey = SymmetricKey(data: Data(hash.makeIterator()))
let sealedBox: ChaChaPoly.SealedBox = try ChaChaPoly.seal(keySpec, using: key, nonce: nonce, authenticating: Data())
let keyInfoPath: String = "\(NSTemporaryDirectory())key.enc"
let encryptedKeyBase64: String = sealedBox.combined.base64EncodedString()
try encryptedKeyBase64.write(toFile: keyInfoPath, atomically: true, encoding: .utf8)
return (
Storage.databasePath,
keyInfoPath
)
}
}
#endif

View File

@ -16,4 +16,5 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
#import <SessionUtilitiesKit/UIImage+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
#import <SessionUtilitiesKit/OWSBackgroundTask.h>
#import <SessionUtilitiesKit/CExceptionHelper.h>

View File

@ -0,0 +1,16 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
#ifndef __CExceptionHelper_h__
#define __CExceptionHelper_h__
#import <Foundation/Foundation.h>
#define noEscape __attribute__((noescape))
@interface CExceptionHelper: NSObject
+ (BOOL)performSafely:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
@end
#endif

View File

@ -0,0 +1,36 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// This logic is not foolproof and may result in memory-leaks, when possible we should look to remove this
// and use the native C++ <-> Swift interoperability coming with Swift 5.9
//
// This solution was sourced from the following link, for more information please refer to this thread:
// https://forums.swift.org/t/pitch-a-swift-representation-for-thrown-and-caught-exceptions/54583
#import "CExceptionHelper.h"
#include <exception>
@implementation CExceptionHelper
+ (BOOL)performSafely:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
try {
tryBlock();
return YES;
}
catch(NSException* e) {
*error = [[NSError alloc] initWithDomain:e.name code:-1 userInfo:e.userInfo];
return NO;
}
catch (std::exception& e) {
NSString* what = [NSString stringWithUTF8String: e.what()];
NSDictionary* userInfo = @{NSLocalizedDescriptionKey : what};
*error = [[NSError alloc] initWithDomain:@"cpp_exception" code:-2 userInfo:userInfo];
return NO;
}
catch(...) {
NSDictionary* userInfo = @{NSLocalizedDescriptionKey:@"Other C++ exception"};
*error = [[NSError alloc] initWithDomain:@"cpp_exception" code:-3 userInfo:userInfo];
return NO;
}
}
@end