Adding support for a few more properties group members

Added 'name' value to updated groups USER_GROUP entry
Added ConvoInfoVolatile for updated groups
Cleaned up a bunch of direct sodium usages
Updated the code to create GroupMember entries based on the GROUP_MEMBERS config
Updated to the latest libSession
Fixed a bunch of group authentication issues
Fixed a minor threading issue
Fixed an issue with the PreparedRequest type conversion
This commit is contained in:
Morgan Pretty 2023-09-08 17:07:51 +10:00
parent b31afa89e1
commit 0982526057
39 changed files with 794 additions and 492 deletions

@ -1 +1 @@
Subproject commit bb7a2cfa1bbbfe94db7a69ffc4875cdf48330432
Subproject commit e21302b598b5dde44fd72566d8b89d4d41cbb9ce

View File

@ -524,7 +524,7 @@
FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */; };
FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* NetworkType.swift */; };
FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1E2A65269C0000B97C /* Crypto.swift */; };
FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */; };
FD23CE222A661D000000B97C /* Crypto+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* Crypto+OpenGroupAPI.swift */; };
FD23CE242A675C440000B97C /* Crypto+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */; };
FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */; };
FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; };
@ -752,6 +752,7 @@
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; };
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; };
FD9AECA72AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA62AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift */; };
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
@ -1762,7 +1763,7 @@
FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationRequirement.swift; sourceTree = "<group>"; };
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = "<group>"; };
FD23CE1E2A65269C0000B97C /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = "<group>"; };
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+Crypto.swift"; sourceTree = "<group>"; };
FD23CE212A661D000000B97C /* Crypto+OpenGroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroupAPI.swift"; sourceTree = "<group>"; };
FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionMessagingKit.swift"; sourceTree = "<group>"; };
FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependenciesSpec.swift; sourceTree = "<group>"; };
FD23CE272A67755C0000B97C /* MockCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrypto.swift; sourceTree = "<group>"; };
@ -1945,6 +1946,7 @@
FD96F3A629DBD43D00401309 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = "<group>"; };
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; };
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; };
FD9AECA62AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = "<group>"; };
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; };
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
@ -2694,6 +2696,8 @@
B8A582AC258C653C00AFD84C /* Crypto */ = {
isa = PBXGroup;
children = (
FD23CE1E2A65269C0000B97C /* Crypto.swift */,
FD9AECA62AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift */,
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */,
B88FA7FA26114EA70049422F /* Hex.swift */,
FDE658A229418E2F00A33BC1 /* KeyPair.swift */,
@ -3710,7 +3714,6 @@
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */,
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */,
FD23CE1E2A65269C0000B97C /* Crypto.swift */,
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */,
FD09796A27F6C67500936362 /* Failable.swift */,
FD47E0AE2AA692F400A55E41 /* JSONDecoder+Utilities.swift */,
@ -3883,7 +3886,7 @@
FD23CE202A661CE80000B97C /* Crypto */ = {
isa = PBXGroup;
children = (
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */,
FD23CE212A661D000000B97C /* Crypto+OpenGroupAPI.swift */,
);
path = Crypto;
sourceTree = "<group>";
@ -5975,6 +5978,7 @@
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */,
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */,
FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */,
FD9AECA72AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift in Sources */,
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */,
@ -6202,7 +6206,7 @@
FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */,
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */,
FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */,
FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */,
FD23CE222A661D000000B97C /* Crypto+OpenGroupAPI.swift in Sources */,
FD245C652850665400B966DD /* ClosedGroupControlMessage.swift in Sources */,
FDB5DAD12A94838C002C8721 /* GroupInviteMemberJob.swift in Sources */,
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */,

View File

@ -82,11 +82,11 @@ class ScreenLockUI {
///
/// * The user is locked out by default on app launch.
/// * The user is also locked out if the app is sent to the background
private var isScreenLockLocked: Bool = false
private var isScreenLockLocked: Atomic<Bool> = Atomic(false)
// Determines what the state of the app should be.
private var desiredUIState: ScreenLockViewController.State {
if isScreenLockLocked {
if isScreenLockLocked.wrappedValue {
if appIsInactiveOrBackground {
Logger.verbose("desiredUIState: screen protection 1.")
return .protection
@ -167,8 +167,13 @@ class ScreenLockUI {
// It's not safe to access OWSScreenLock.isScreenLockEnabled
// until the app is ready.
AppReadiness.runNowOrWhenAppWillBecomeReady { [weak self] in
self?.isScreenLockLocked = Dependencies()[singleton: .storage][.isScreenLockEnabled]
self?.ensureUI()
DispatchQueue.global(qos: .background).async {
self?.isScreenLockLocked.mutate { $0 = Dependencies()[singleton: .storage][.isScreenLockEnabled] }
DispatchQueue.main.async {
self?.ensureUI()
}
}
}
}
@ -189,13 +194,13 @@ class ScreenLockUI {
Logger.verbose("tryToActivateScreenLockUponBecomingActive NO 1")
return;
}
guard !isScreenLockLocked else {
guard !isScreenLockLocked.wrappedValue else {
// Screen lock is already activated.
Logger.verbose("tryToActivateScreenLockUponBecomingActive NO 2")
return;
}
self.isScreenLockLocked = true
self.isScreenLockLocked.mutate { $0 = true }
}
/// Ensure that:
@ -234,7 +239,7 @@ class ScreenLockUI {
success: { [weak self] in
Logger.info("unlock screen lock succeeded.")
self?.isShowingScreenLockUI = false
self?.isScreenLockLocked = false
self?.isScreenLockLocked.mutate { $0 = false }
self?.didUnlockJustSucceed = true
self?.ensureUI()
},
@ -372,11 +377,15 @@ class ScreenLockUI {
return;
}
self.isScreenLockLocked = Dependencies()[singleton: .storage][.isScreenLockEnabled]
DispatchQueue.global(qos: .background).async {
self.isScreenLockLocked.mutate { $0 = Dependencies()[singleton: .storage][.isScreenLockEnabled] }
// NOTE: this notifications fires _before_ applicationDidBecomeActive,
// which is desirable. Don't assume that though; call ensureUI
// just in case it's necessary.
self.ensureUI()
DispatchQueue.main.async {
// NOTE: this notifications fires _before_ applicationDidBecomeActive,
// which is desirable. Don't assume that though; call ensureUI
// just in case it's necessary.
self.ensureUI()
}
}
}
}

View File

@ -201,7 +201,7 @@ enum MockDataGenerator {
threadId: randomGroupPublicKey,
name: groupName,
formationTimestamp: timestampNow,
approved: true
invited: false
)
.saved(db)

View File

@ -21,9 +21,9 @@ enum _017_GroupsRebuildChanges: Migration {
.defaults(to: 0)
t.add(.groupIdentityPrivateKey, .blob)
t.add(.authData, .blob)
t.add(.approved, .boolean)
t.add(.invited, .boolean)
.notNull()
.defaults(to: true)
.defaults(to: false)
}
Storage.update(progress: 1, for: self, in: target, using: dependencies)

View File

@ -29,7 +29,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
case groupIdentityPrivateKey
case authData
case approved
case invited
}
public var id: String { threadId } // Identifiable
@ -62,8 +62,8 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
/// **Note:** This will be `null` if the `groupIdentityPrivateKey` is set
public let authData: Data?
/// A flag indicating whether the user has approved the group invitation
public let approved: Bool
/// A flag indicating whether this group is in the "invite" state
public let invited: Bool
// MARK: - Relationships
@ -111,7 +111,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
lastDisplayPictureUpdate: TimeInterval = 0,
groupIdentityPrivateKey: Data? = nil,
authData: Data? = nil,
approved: Bool
invited: Bool
) {
self.threadId = threadId
self.name = name
@ -122,7 +122,7 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
self.lastDisplayPictureUpdate = lastDisplayPictureUpdate
self.groupIdentityPrivateKey = groupIdentityPrivateKey
self.authData = authData
self.approved = approved
self.invited = invited
}
}
@ -179,6 +179,36 @@ public extension ClosedGroup {
}
}
static func approveGroup(
_ db: Database,
group: ClosedGroup,
calledFromConfigHandling: Bool,
using dependencies: Dependencies
) throws {
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db, using: dependencies) else {
throw MessageReceiverError.noUserED25519KeyPair
}
if !group.invited {
try ClosedGroup
.filter(id: group.id)
.updateAllAndConfig(
db,
ClosedGroup.Columns.invited.set(to: false)
)
}
try SessionUtil.createGroupState(
groupId: group.id,
userED25519KeyPair: userED25519KeyPair,
groupIdentityPrivateKey: group.groupIdentityPrivateKey,
authData: group.authData,
using: dependencies
)
// Start polling
dependencies[singleton: .closedGroupPoller].startIfNeeded(for: group.id, using: dependencies)
}
static func removeKeysAndUnsubscribe(
_ db: Database? = nil,
threadId: String,

View File

@ -34,72 +34,6 @@ public extension Crypto.Action {
}
}
// MARK: - AeadXChaCha20Poly1305Ietf
public extension Crypto.Size {
static let aeadXChaCha20KeyBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20KeyBytes") {
Sodium().aead.xchacha20poly1305ietf.KeyBytes
}
static let aeadXChaCha20ABytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20ABytes") {
Sodium().aead.xchacha20poly1305ietf.ABytes
}
}
public extension Crypto.Action {
/// This method is the same as the standard AeadXChaCha20Poly1305Ietf `encrypt` method except it allows the
/// specification of a nonce which allows for deterministic behaviour with unit testing
static func encryptAeadXChaCha20(
message: Bytes,
secretKey: Bytes,
nonce: Bytes,
additionalData: Bytes? = nil,
using dependencies: Dependencies
) -> Crypto.Action {
return Crypto.Action(
id: "encryptAeadXChaCha20",
args: [message, secretKey, nonce, additionalData]
) {
guard secretKey.count == dependencies[singleton: .crypto].size(.aeadXChaCha20KeyBytes) else { return nil }
var authenticatedCipherText = Bytes(
repeating: 0,
count: message.count + dependencies[singleton: .crypto].size(.aeadXChaCha20ABytes)
)
var authenticatedCipherTextLen: UInt64 = 0
let result = crypto_aead_xchacha20poly1305_ietf_encrypt(
&authenticatedCipherText, &authenticatedCipherTextLen,
message, UInt64(message.count),
additionalData, UInt64(additionalData?.count ?? 0),
nil, nonce, secretKey
)
guard result == 0 else { return nil }
return authenticatedCipherText
}
}
static func decryptAeadXChaCha20(
authenticatedCipherText: Bytes,
secretKey: Bytes,
nonce: Bytes,
additionalData: Bytes? = nil
) -> Crypto.Action {
return Crypto.Action(
id: "decryptAeadXChaCha20",
args: [authenticatedCipherText, secretKey, nonce, additionalData]
) {
return Sodium().aead.xchacha20poly1305ietf.decrypt(
authenticatedCipherText: authenticatedCipherText,
secretKey: secretKey,
nonce: nonce,
additionalData: additionalData
)
}
}
}
// MARK: - Blinding
/// These extenion methods are used to generate a sign "blinded" messages

View File

@ -25,7 +25,7 @@ extension MessageReceiver {
name: String?,
authData: Data?,
created: Int64,
approved: Bool,
invited: Bool,
calledFromConfigHandling: Bool,
using dependencies: Dependencies
) throws {
@ -39,7 +39,7 @@ extension MessageReceiver {
formationTimestamp: TimeInterval(created),
groupIdentityPrivateKey: groupIdentityPrivateKey,
authData: authData,
approved: approved
invited: invited
).saved(db)
if !calledFromConfigHandling {
@ -51,19 +51,19 @@ extension MessageReceiver {
name: name,
authData: authData,
joinedAt: created,
approved: approved,
invited: invited,
using: dependencies
)
}
// Only start polling and subscribe for PNs if the user has approved the group
guard approved else { return }
// Start polling
dependencies[singleton: .closedGroupPoller].startIfNeeded(for: groupIdentityPublicKey, using: dependencies)
// Resubscribe for group push notifications
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
// If the group is not in the invite state then handle the approval process
guard !invited else { return }
try ClosedGroup.approveGroup(
db,
group: closedGroup,
calledFromConfigHandling: calledFromConfigHandling,
using: dependencies
)
}
}

View File

@ -163,7 +163,7 @@ extension MessageReceiver {
threadId: groupPublicKey,
name: name,
formationTimestamp: (TimeInterval(formationTimestampMs) / 1000),
approved: true // Legacy groups are always approved
invited: false // Legacy groups are never in the "invite" state
).saved(db)
// Clear the zombie list if the group wasn't active (ie. had no keys)

View File

@ -2,7 +2,6 @@
import Foundation
import GRDB
import Sodium
import SessionSnodeKit
import SessionUtilitiesKit
@ -89,8 +88,6 @@ extension MessageReceiver {
// Need to check if the blinded id matches for open groups
switch senderSessionId.prefix {
case .blinded15, .blinded25:
let sodium: Sodium = Sodium()
guard
let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db),
let blindedKeyPair: KeyPair = dependencies[singleton: .crypto].generate(

View File

@ -65,9 +65,8 @@ extension MessageSender {
// Prepare the notification subscription
let preparedNotificationSubscription = try? PushNotificationAPI
.preparedSubscribe(
db,
publicKey: createdInfo.group.id,
subkey: nil,
ed25519KeyPair: createdInfo.identityKeyPair,
using: dependencies
)
@ -160,4 +159,28 @@ extension MessageSender {
.map { _, thread, _, _, _ in thread }
.eraseToAnyPublisher()
}
public static func updateGroup(
groupIdentityPublicKey: String,
name: String,
displayPicture: SignalAttachment?,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
guard SessionId.Prefix(from: groupIdentityPublicKey) == .group else {
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
.eraseToAnyPublisher()
}
return dependencies[singleton: .storage]
.writePublisher { db in
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: groupIdentityPublicKey) else {
throw MessageSenderError.invalidClosedGroupUpdate
}
// Update name if needed
if name != closedGroup.name {
// Update the group
_ = try ClosedGroup
.filter(id: groupIdentityPublicKey)
.updateAllAndConfig(db, ClosedGroup.Columns.name.set(to: name))
}

View File

@ -42,7 +42,7 @@ extension MessageSender {
threadId: legacyGroupPublicKey,
name: name,
formationTimestamp: formationTimestamp,
approved: true // Legacy groups are always approved
invited: false // Legacy groups are never in the "invite" state
).insert(db)
// Store the key pair

View File

@ -2,6 +2,7 @@
import Foundation
import SessionSnodeKit
import SessionUtilitiesKit
extension PushNotificationAPI {
struct SubscribeRequest: Encodable {
@ -32,9 +33,6 @@ extension PushNotificationAPI {
case notificationsEncryptionKey = "enc_key"
}
/// The 33-byte account being subscribed to; typically a session ID.
private let pubkey: String
/// List of integer namespace (-32768 through 32767). These must be sorted in ascending order.
private let namespaces: [SnodeAPI.Namespace]
@ -50,41 +48,28 @@ extension PushNotificationAPI {
/// it is permitted for this to change, it is recommended that the device generate this once and persist it.
private let notificationsEncryptionKey: Data
/// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth
private let subkey: String?
/// The authentication information needed to subscribe for notifications
private let authInfo: SnodeAPI.AuthenticationInfo
/// The signature unix timestamp (seconds, not ms)
private let timestamp: Int64
/// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session
/// ID. When not 05, this field should not be provided.
private let ed25519PublicKey: [UInt8]
/// Secret key used to generate the signature (**Not** sent with the request)
private let ed25519SecretKey: [UInt8]
// MARK: - Initialization
init(
pubkey: String,
namespaces: [SnodeAPI.Namespace],
includeMessageData: Bool,
serviceInfo: ServiceInfo,
notificationsEncryptionKey: Data,
subkey: String?,
timestamp: TimeInterval,
ed25519PublicKey: [UInt8],
ed25519SecretKey: [UInt8]
authInfo: SnodeAPI.AuthenticationInfo,
timestamp: TimeInterval
) {
self.pubkey = pubkey
self.namespaces = namespaces
self.includeMessageData = includeMessageData
self.serviceInfo = serviceInfo
self.notificationsEncryptionKey = notificationsEncryptionKey
self.subkey = subkey
self.authInfo = authInfo
self.timestamp = Int64(timestamp) // Server expects rounded seconds
self.ed25519PublicKey = ed25519PublicKey
self.ed25519SecretKey = ed25519SecretKey
}
// MARK: - Coding
@ -93,10 +78,7 @@ extension PushNotificationAPI {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
// Generate the signature for the request for encoding
let signatureBase64: String = try generateSignature().toBase64()
try container.encode(pubkey, forKey: .pubkey)
try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey)
try container.encodeIfPresent(subkey, forKey: .subkey)
let signatureBase64: String = try generateSignature(using: encoder.dependencies).toBase64()
try container.encode(namespaces.map { $0.rawValue}.sorted(), forKey: .namespaces)
try container.encode(includeMessageData, forKey: .includeMessageData)
try container.encode(timestamp, forKey: .timestamp)
@ -104,11 +86,23 @@ extension PushNotificationAPI {
try container.encode(Service.apns, forKey: .service)
try container.encode(serviceInfo, forKey: .serviceInfo)
try container.encode(notificationsEncryptionKey.toHexString(), forKey: .notificationsEncryptionKey)
switch authInfo {
case .standard(let pubkey, let ed25519KeyPair):
try container.encode(pubkey, forKey: .pubkey)
try container.encode(ed25519KeyPair.publicKey.toHexString(), forKey: .ed25519PublicKey)
case .groupAdmin(let pubkey, _):
try container.encode(pubkey, forKey: .pubkey)
case .groupMember(let pubkey, let authData):
try container.encode(pubkey, forKey: .pubkey)
}
}
// MARK: - Abstract Methods
func generateSignature() throws -> [UInt8] {
func generateSignature(using dependencies: Dependencies) throws -> [UInt8] {
/// The signature data collected and stored here is used by the PN server to subscribe to the swarms
/// for the given account; the specific rules are governed by the storage server, but in general:
///
@ -125,7 +119,7 @@ extension PushNotificationAPI {
/// comma-delimited list of namespaces that should be subscribed to, in the same sorted order as
/// the `namespaces` parameter.
let verificationBytes: [UInt8] = "MONITOR".bytes
.appending(contentsOf: pubkey.bytes)
.appending(contentsOf: authInfo.publicKey.bytes)
.appending(contentsOf: "\(timestamp)".bytes)
.appending(contentsOf: (includeMessageData ? "1" : "0").bytes)
.appending(
@ -137,17 +131,7 @@ extension PushNotificationAPI {
.bytes
)
// TODO: Need to add handling for subkey auth
guard
let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature(
message: verificationBytes,
secretKey: ed25519SecretKey
)
else {
throw SnodeAPIError.signingFailed
}
return signatureBytes
return try authInfo.generateSignature(with: verificationBytes, using: dependencies)
}
}
}

View File

@ -2,6 +2,7 @@
import Foundation
import SessionSnodeKit
import SessionUtilitiesKit
extension PushNotificationAPI {
struct UnsubscribeRequest: Encodable {
@ -29,42 +30,26 @@ extension PushNotificationAPI {
case serviceInfo = "service_info"
}
/// The 33-byte account being subscribed to; typically a session ID.
private let pubkey: String
/// Dict of service-specific data; typically this includes just a "token" field with a device-specific token, but different services in the
/// future may have different input requirements.
private let serviceInfo: ServiceInfo
/// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth
private let subkey: String?
/// The authentication information needed to subscribe for notifications
private let authInfo: SnodeAPI.AuthenticationInfo
/// The signature unix timestamp (seconds, not ms)
private let timestamp: Int64
/// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session
/// ID. When not 05, this field should not be provided.
private let ed25519PublicKey: [UInt8]
/// Secret key used to generate the signature (**Not** sent with the request)
private let ed25519SecretKey: [UInt8]
// MARK: - Initialization
init(
pubkey: String,
serviceInfo: ServiceInfo,
subkey: String?,
timestamp: TimeInterval,
ed25519PublicKey: [UInt8],
ed25519SecretKey: [UInt8]
authInfo: SnodeAPI.AuthenticationInfo,
timestamp: TimeInterval
) {
self.pubkey = pubkey
self.serviceInfo = serviceInfo
self.subkey = subkey
self.authInfo = authInfo
self.timestamp = Int64(timestamp) // Server expects rounded seconds
self.ed25519PublicKey = ed25519PublicKey
self.ed25519SecretKey = ed25519SecretKey
}
// MARK: - Coding
@ -73,39 +58,38 @@ extension PushNotificationAPI {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
// Generate the signature for the request for encoding
let signatureBase64: String = try generateSignature().toBase64()
try container.encode(pubkey, forKey: .pubkey)
try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey)
try container.encodeIfPresent(subkey, forKey: .subkey)
let signatureBase64: String = try generateSignature(using: encoder.dependencies).toBase64()
try container.encode(timestamp, forKey: .timestamp)
try container.encode(signatureBase64, forKey: .signatureBase64)
try container.encode(Service.apns, forKey: .service)
try container.encode(serviceInfo, forKey: .serviceInfo)
switch authInfo {
case .standard(let pubkey, let ed25519KeyPair):
try container.encode(pubkey, forKey: .pubkey)
try container.encode(ed25519KeyPair.publicKey.toHexString(), forKey: .ed25519PublicKey)
case .groupAdmin(let pubkey, _):
try container.encode(pubkey, forKey: .pubkey)
case .groupMember(let pubkey, let authData):
try container.encode(pubkey, forKey: .pubkey)
}
}
// MARK: - Abstract Methods
func generateSignature() throws -> [UInt8] {
func generateSignature(using dependencies: Dependencies) throws -> [UInt8] {
/// A signature is signed using the account's Ed25519 private key (or Ed25519 subkey, if using
/// subkey authentication with a `subkey_tag`, for future closed group subscriptions), and signs the value:
/// `"UNSUBSCRIBE" || HEX(ACCOUNT) || SIG_TS`
///
/// Where `SIG_TS` is the `sig_ts` value as a base-10 string and must be within 24 hours of the current time.
let verificationBytes: [UInt8] = "UNSUBSCRIBE".bytes
.appending(contentsOf: pubkey.bytes)
.appending(contentsOf: authInfo.publicKey.bytes)
.appending(contentsOf: "\(timestamp)".data(using: .ascii)?.bytes)
// TODO: Need to add handling for subkey auth
guard
let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature(
message: verificationBytes,
secretKey: ed25519SecretKey
)
else {
throw SnodeAPIError.signingFailed
}
return signatureBytes
return try authInfo.generateSignature(with: verificationBytes, using: dependencies)
}
}
}

View File

@ -3,12 +3,10 @@
import Foundation
import Combine
import GRDB
import Sodium
import SessionSnodeKit
import SessionUtilitiesKit
public enum PushNotificationAPI {
internal static let sodium: Atomic<Sodium> = Atomic(Sodium())
private static let keychainService: String = "PNKeyChainService"
private static let encryptionKeyKey: String = "PNEncryptionKeyKey"
private static let encryptionKeyLength: Int = 32
@ -45,16 +43,11 @@ public enum PushNotificationAPI {
return dependencies[singleton: .storage]
.readPublisher(using: dependencies) { db -> SubscribeAllPreparedRequests in
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw SnodeAPIError.noKeyPair
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let preparedUserRequest = try PushNotificationAPI
.preparedSubscribe(
db,
publicKey: currentUserPublicKey,
subkey: nil,
ed25519KeyPair: userED25519KeyPair,
using: dependencies
)
.handleEvents(
@ -83,7 +76,6 @@ public enum PushNotificationAPI {
using: dependencies
)
return (
preparedUserRequest,
preparedLegacyGroupRequest
@ -129,6 +121,7 @@ public enum PushNotificationAPI {
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let preparedUserRequest = try PushNotificationAPI
.preparedUnsubscribe(
db,
token: token,
publicKey: currentUserPublicKey,
subkey: nil,
@ -177,9 +170,8 @@ public enum PushNotificationAPI {
// MARK: - Prepared Requests
public static func preparedSubscribe(
_ db: Database,
publicKey: String,
subkey: String?,
ed25519KeyPair: KeyPair,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<SubscribeResponse> {
guard
@ -198,8 +190,12 @@ public enum PushNotificationAPI {
method: .post,
endpoint: .subscribe,
body: SubscribeRequest(
pubkey: publicKey,
namespaces: [.default],
namespaces: {
switch SessionId.Prefix(from: publicKey) {
case .group: return [.default]
default: return [.default, .configConvoInfoVolatile]
}
}(),
// Note: Unfortunately we always need the message content because without the content
// control messages can't be distinguished from visible messages which results in the
// 'generic' notification being shown when receiving things like typing indicator updates
@ -208,10 +204,8 @@ public enum PushNotificationAPI {
token: token
),
notificationsEncryptionKey: notificationsEncryptionKey,
subkey: subkey,
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
ed25519PublicKey: ed25519KeyPair.publicKey,
ed25519SecretKey: ed25519KeyPair.secretKey
authInfo: try SnodeAPI.AuthenticationInfo(db, threadId: publicKey, using: dependencies),
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) // Seconds
)
),
responseType: SubscribeResponse.self,
@ -233,6 +227,7 @@ public enum PushNotificationAPI {
}
public static func preparedUnsubscribe(
_ db: Database,
token: Data,
publicKey: String,
subkey: String?,
@ -245,14 +240,11 @@ public enum PushNotificationAPI {
method: .post,
endpoint: .unsubscribe,
body: UnsubscribeRequest(
pubkey: publicKey,
serviceInfo: UnsubscribeRequest.ServiceInfo(
token: token.toHexString()
),
subkey: nil,
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
ed25519PublicKey: ed25519KeyPair.publicKey,
ed25519SecretKey: ed25519KeyPair.secretKey
authInfo: try SnodeAPI.AuthenticationInfo(db, threadId: publicKey, using: dependencies),
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) // Seconds
)
),
responseType: UnsubscribeResponse.self,

View File

@ -43,17 +43,18 @@ public final class ClosedGroupPoller: Poller {
public func start(using dependencies: Dependencies = Dependencies()) {
// Fetch all closed groups (excluding any don't contain the current user as a
// GroupMemeber as the user is no longer a member of those)
// GroupMemeber and any which are in the 'invited' state)
dependencies[singleton: .storage]
.read { db in
.read { db -> Set<String> in
try ClosedGroup
.select(.threadId)
.filter(ClosedGroup.Columns.invited == false)
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db, using: dependencies))
)
.asRequest(of: String.self)
.fetchAll(db)
.fetchSet(db)
}
.defaulting(to: [])
.forEach { [weak self] publicKey in

View File

@ -202,7 +202,26 @@ internal extension SessionUtil {
}
convo_info_volatile_set_community(conf, &community)
case .group: return
case .group:
var group: convo_info_volatile_group = convo_info_volatile_group()
guard convo_info_volatile_get_or_construct_group(conf, &group, &cThreadId) else {
/// It looks like there are some situations where this object might not get created correctly (and
/// will throw due to the implicit unwrapping) as a result we put it in a guard and throw instead
SNLog("Unable to upsert group volatile info to SessionUtil: \(config.lastError)")
throw SessionUtilError.getOrConstructFailedUnexpectedly
}
threadInfo.changes.forEach { change in
switch change {
case .lastReadTimestampMs(let lastReadMs):
group.last_read = max(group.last_read, lastReadMs)
case .markedAsUnread(let unread):
group.unread = unread
}
}
convo_info_volatile_set_group(conf, &group)
}
}
}
@ -296,6 +315,12 @@ internal extension SessionUtil {
) { config in
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
volatileGroupIds.forEach { groupId in
var cGroupId: [CChar] = groupId.cArray.nullTerminated()
// Don't care if the data doesn't exist
convo_info_volatile_erase_group(conf, &cGroupId)
}
}
}
@ -402,7 +427,15 @@ public extension SessionUtil {
return (convoCommunity.last_read >= timestampMs)
case .group: return false
case .group:
var cThreadId: [CChar] = threadId.cArray.nullTerminated()
var group: convo_info_volatile_group = convo_info_volatile_group()
guard convo_info_volatile_get_group(conf, &group, &cThreadId) else {
return false
}
return (group.last_read >= timestampMs)
}
}
.defaulting(to: false) // If we don't have a config then just assume it's unread
@ -561,6 +594,7 @@ public extension SessionUtil {
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()
var group: convo_info_volatile_group = convo_info_volatile_group()
let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf)
while !convo_info_volatile_iterator_done(convoIterator) {
@ -615,6 +649,18 @@ public extension SessionUtil {
)
)
}
else if convo_info_volatile_it_is_group(convoIterator, &group) {
result.append(
VolatileThreadInfo(
threadId: String(libSessionVal: group.group_id),
variant: .group,
changes: [
.markedAsUnread(group.unread),
.lastReadTimestampMs(group.last_read)
]
)
)
}
else {
SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update")
}

View File

@ -21,10 +21,104 @@ internal extension SessionUtil {
) throws {
guard config.needsDump else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
var infiniteLoopGuard: Int = 0
var result: [MemberData] = []
var member: config_group_member = config_group_member()
let membersIterator: UnsafeMutablePointer<groups_members_iterator> = groups_members_iterator_new(conf)
while !groups_members_iterator_done(membersIterator, &member) {
try SessionUtil.checkLoopLimitReached(&infiniteLoopGuard, for: .groupMembers)
let memberId: String = String(cString: withUnsafeBytes(of: member.session_id) { [UInt8]($0) }
.map { CChar($0) }
.nullTerminated()
)
let profilePictureUrl: String? = String(libSessionVal: member.profile_pic.url, nullIfEmpty: true)
let profileResult: Profile = Profile(
id: memberId,
name: String(libSessionVal: member.name),
lastNameUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000),
nickname: nil,
profilePictureUrl: profilePictureUrl,
profileEncryptionKey: (profilePictureUrl == nil ? nil :
Data(
libSessionVal: member.profile_pic.key,
count: ProfileManager.avatarAES256KeyByteLength
)
),
lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000),
lastBlocksCommunityMessageRequests: 0
)
result.append(
MemberData(
memberId: memberId,
profile: profileResult,
admin: member.admin,
invited: member.invited,
promoted: member.promoted
)
)
groups_members_iterator_advance(membersIterator)
}
groups_members_iterator_free(membersIterator) // Need to free the iterator
// Get the two member sets
let existingMembers: Set<GroupMember> = (try? GroupMember
.filter(GroupMember.Columns.groupId == groupIdentityPublicKey)
.fetchSet(db))
.defaulting(to: [])
let updatedMembers: Set<GroupMember> = result
.map {
GroupMember(
groupId: groupIdentityPublicKey,
profileId: $0.memberId,
role: ($0.admin ? .admin : .standard),
// TODO: Other properties
isHidden: false
)
}
.asSet()
let updatedStandardMemberIds: Set<String> = updatedMembers
.filter { $0.role == .standard }
.map { $0.profileId }
.asSet()
let updatedAdminMemberIds: Set<String> = updatedMembers
.filter { $0.role == .admin }
.map { $0.profileId }
.asSet()
// Add in any new members and remove any removed members
try updatedMembers
.subtracting(existingMembers)
.forEach { try $0.save(db) }
try GroupMember
.filter(
GroupMember.Columns.groupId == groupIdentityPublicKey && (
GroupMember.Columns.role == GroupMember.Role.standard &&
!updatedStandardMemberIds.contains(GroupMember.Columns.profileId)
) || (
GroupMember.Columns.role == GroupMember.Role.admin &&
!updatedAdminMemberIds.contains(GroupMember.Columns.profileId)
)
)
.deleteAll(db)
}
}
// MARK: - Outgoing Changes
internal extension SessionUtil {
// MARK: - MemberData
private struct MemberData {
let memberId: String
let profile: Profile?
let admin: Bool
let invited: Int32
let promoted: Int32
}

View File

@ -157,7 +157,7 @@ internal extension SessionUtil {
displayPictureEncryptionKey: displayPictureEncryptionKey,
lastDisplayPictureUpdate: creationTimestamp,
groupIdentityPrivateKey: Data(groupIdentityPrivateKey),
approved: true
invited: false
),
finalMembers.map { memberId, info -> GroupMember in
GroupMember(
@ -210,7 +210,7 @@ internal extension SessionUtil {
name: group.name,
authData: group.authData,
joinedAt: Int64(floor(group.formationTimestamp)),
approved: group.approved,
invited: group.invited,
using: dependencies
)
}

View File

@ -130,6 +130,7 @@ internal extension SessionUtil {
nullIfEmpty: true
)
),
name: String(libSessionVal: group.name),
authData: (!group.have_auth_data ? nil :
Data(
libSessionVal: group.auth_data,
@ -138,7 +139,8 @@ internal extension SessionUtil {
)
),
priority: group.priority,
joinedAt: group.joined_at
joinedAt: group.joined_at,
invited: group.invited
)
)
}
@ -436,7 +438,7 @@ internal extension SessionUtil {
name: group.name,
authData: group.authData,
created: Int64((group.joinedAt ?? (latestConfigSentTimestampMs / 1000))),
approved: (group.approved == true),
invited: (group.invited == true),
calledFromConfigHandling: true,
using: dependencies
)
@ -456,8 +458,8 @@ internal extension SessionUtil {
(existingGroups[group.groupIdentityPublicKey]?.groupIdentityPrivateKey == group.groupIdentityPrivateKey ? nil :
ClosedGroup.Columns.groupIdentityPrivateKey.set(to: group.groupIdentityPrivateKey)
),
(existingGroups[group.groupIdentityPublicKey]?.approved == group.approved ? nil :
ClosedGroup.Columns.approved.set(to: (group.approved ?? false))
(existingGroups[group.groupIdentityPublicKey]?.invited == group.invited ? nil :
ClosedGroup.Columns.invited.set(to: (group.invited ?? false))
)
].compactMap { $0 }
@ -644,10 +646,10 @@ internal extension SessionUtil {
try groups
.forEach { group in
var cGroupId: [CChar] = group.groupIdentityPublicKey.cArray.nullTerminated()
var cGroupPubkey: [CChar] = group.groupIdentityPublicKey.cArray.nullTerminated()
var userGroup: ugroups_group_info = ugroups_group_info()
guard user_groups_get_or_construct_group(conf, &userGroup, &cGroupId) else {
guard user_groups_get_or_construct_group(conf, &userGroup, &cGroupPubkey) else {
/// It looks like there are some situations where this object might not get created correctly (and
/// will throw due to the implicit unwrapping) as a result we put it in a guard and throw instead
SNLog("Unable to upsert group conversation to SessionUtil: \(config.lastError)")
@ -671,8 +673,17 @@ internal extension SessionUtil {
// Store the updated group (needs to happen before variables go out of scope)
user_groups_set_group(conf, &userGroup)
}
/// Assign the group name
if let name: String = group.name {
userGroup.name = name.toLibSession()
// Store the updated group (needs to happen before variables go out of scope)
user_groups_set_group(conf, &userGroup)
}
// Store the updated group (can't be sure if we made any changes above)
userGroup.invited = (group.invited ?? userGroup.invited)
userGroup.joined_at = (group.joinedAt ?? userGroup.joined_at)
userGroup.priority = (group.priority ?? userGroup.priority)
user_groups_set_group(conf, &userGroup)
@ -956,7 +967,7 @@ public extension SessionUtil {
name: String?,
authData: Data?,
joinedAt: Int64,
approved: Bool,
invited: Bool,
using dependencies: Dependencies
) throws {
try SessionUtil.performAndPushChange(
@ -973,7 +984,7 @@ public extension SessionUtil {
name: name,
authData: authData,
joinedAt: joinedAt,
approved: approved
invited: invited
)
],
in: config
@ -987,7 +998,7 @@ public extension SessionUtil {
groupIdentityPrivateKey: Data? = nil,
name: String? = nil,
authData: Data? = nil,
approved: Bool? = nil,
invited: Bool? = nil,
using dependencies: Dependencies
) throws {
try SessionUtil.performAndPushChange(
@ -1003,7 +1014,7 @@ public extension SessionUtil {
groupIdentityPrivateKey: groupIdentityPrivateKey,
name: name,
authData: authData,
approved: approved
invited: invited
)
],
in: config
@ -1174,7 +1185,7 @@ extension SessionUtil {
let authData: Data?
let priority: Int32?
let joinedAt: Int64?
let approved: Bool?
let invited: Bool?
init(
groupIdentityPublicKey: String,
@ -1183,7 +1194,7 @@ extension SessionUtil {
authData: Data? = nil,
priority: Int32? = nil,
joinedAt: Int64? = nil,
approved: Bool? = nil
invited: Bool? = nil
) {
self.groupIdentityPublicKey = groupIdentityPublicKey
self.groupIdentityPrivateKey = groupIdentityPrivateKey
@ -1191,7 +1202,7 @@ extension SessionUtil {
self.authData = authData
self.priority = priority
self.joinedAt = joinedAt
self.approved = approved
self.invited = invited
}
}
}

View File

@ -3,21 +3,14 @@
import Foundation
import Sodium
import Clibsodium
import Curve25519Kit
import SessionUtilitiesKit
// MARK: - Generic Hash
public extension Crypto.Action {
static func hash(message: Bytes, key: Bytes?) -> Crypto.Action {
return Crypto.Action(id: "hash", args: [message, key]) {
Sodium().genericHash.hash(message: message, key: key)
}
}
static func hash(message: Bytes, outputLength: Int) -> Crypto.Action {
return Crypto.Action(id: "hashOutputLength", args: [message, outputLength]) {
Sodium().genericHash.hash(message: message, outputLength: outputLength)
return Crypto.Action(id: "hashOutputLength", args: [message, outputLength]) { sodium in
sodium.genericHash.hash(message: message, outputLength: outputLength)
}
}
@ -52,34 +45,16 @@ public extension Crypto.Action {
}
}
// MARK: - Sign
public extension Crypto.Action {
static func toX25519(ed25519PublicKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "toX25519", args: [ed25519PublicKey]) {
Sodium().sign.toX25519(ed25519PublicKey: ed25519PublicKey)
}
}
static func toX25519(ed25519SecretKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "toX25519", args: [ed25519SecretKey]) {
Sodium().sign.toX25519(ed25519SecretKey: ed25519SecretKey)
}
}
}
// MARK: - Box
public extension Crypto.Size {
static let signature: Crypto.Size = Crypto.Size(id: "signature") { Sodium().sign.Bytes }
static let publicKey: Crypto.Size = Crypto.Size(id: "publicKey") { Sodium().sign.PublicKeyBytes }
static let secretKey: Crypto.Size = Crypto.Size(id: "secretKey") { Sodium().sign.SecretKeyBytes }
static let signature: Crypto.Size = Crypto.Size(id: "signature") { $0.sign.Bytes }
}
public extension Crypto.Action {
static func seal(message: Bytes, recipientPublicKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "seal", args: [message, recipientPublicKey]) {
Sodium().box.seal(message: message, recipientPublicKey: recipientPublicKey)
return Crypto.Action(id: "seal", args: [message, recipientPublicKey]) { sodium in
sodium.box.seal(message: message, recipientPublicKey: recipientPublicKey)
}
}
@ -87,8 +62,8 @@ public extension Crypto.Action {
return Crypto.Action(
id: "open",
args: [anonymousCipherText, recipientPublicKey, recipientSecretKey]
) {
Sodium().box.open(
) { sodium in
sodium.box.open(
anonymousCipherText: anonymousCipherText,
recipientPublicKey: recipientPublicKey,
recipientSecretKey: recipientSecretKey
@ -100,62 +75,7 @@ public extension Crypto.Action {
// MARK: - AeadXChaCha20Poly1305Ietf
public extension Crypto.Size {
static let aeadXChaCha20NonceBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20NonceBytes") {
Sodium().aead.xchacha20poly1305ietf.NonceBytes
}
}
// MARK: - Ed25519
public extension Crypto.Action {
static func signEd25519(data: Bytes, keyPair: KeyPair) -> Crypto.Action {
return Crypto.Action(id: "signEd25519", args: [data, keyPair]) {
let ecKeyPair: ECKeyPair = try ECKeyPair(
publicKeyData: Data(keyPair.publicKey),
privateKeyData: Data(keyPair.secretKey)
)
return try Ed25519.sign(Data(data), with: ecKeyPair).bytes
}
}
}
public extension Crypto.Verification {
static func signatureEd25519(_ signature: Data, publicKey: Data, data: Data) -> Crypto.Verification {
return Crypto.Verification(id: "signatureEd25519", args: [signature, publicKey, data]) {
return ((try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true)
}
}
}
public extension Crypto.KeyPairType {
static func x25519KeyPair() -> Crypto.KeyPairType {
return Crypto.KeyPairType(id: "x25519KeyPair") {
let keyPair: ECKeyPair = Curve25519.generateKeyPair()
return KeyPair(publicKey: Array(keyPair.publicKey), secretKey: Array(keyPair.privateKey))
}
}
static func ed25519KeyPair(
seed: Data? = nil,
using dependencies: Dependencies = Dependencies()
) -> Crypto.KeyPairType {
return Crypto.KeyPairType(id: "ed25519KeyPair") {
let pkSize: Int = dependencies[singleton: .crypto].size(.publicKey)
let skSize: Int = dependencies[singleton: .crypto].size(.secretKey)
var edPK: [UInt8] = [UInt8](repeating: 0, count: pkSize)
var edSK: [UInt8] = [UInt8](repeating: 0, count: skSize)
var targetSeed: [UInt8] = ((seed ?? (try? Randomness.generateRandomBytes(numberBytes: skSize)))
.map { Array($0) })
.defaulting(to: [])
// Generate the key
guard Sodium.lib_crypto_sign_ed25519_seed_keypair(&edPK, &edSK, &targetSeed) == 0 else {
return nil
}
return KeyPair(publicKey: edPK, secretKey: edSK)
}
static let aeadXChaCha20NonceBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20NonceBytes") { sodium in
sodium.aead.xchacha20poly1305ietf.NonceBytes
}
}

View File

@ -535,7 +535,7 @@ public struct ProfileManager {
}
}
// Blocks community message requets flag
// Blocks community message requests flag
if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > profile.lastBlocksCommunityMessageRequests {
profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests))
profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp))

View File

@ -60,9 +60,9 @@ extension LibSessionSpec {
expect(config_merge(conf2, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beFalse())
mergeHashes1.forEach { $0?.deallocate() }
mergeData1.forEach { $0?.deallocate() }
pushData1.deallocate()
let namePtr: UnsafePointer<CChar>? = groups_info_get_name(conf)
let namePtr: UnsafePointer<CChar>? = groups_info_get_name(conf2)
expect(namePtr).toNot(beNil())
expect(String(cString: namePtr!)).to(equal("GROUP Name"))
@ -87,7 +87,7 @@ extension LibSessionSpec {
)
expect(pushData2.pointee.seqno).to(equal(2))
expect(pushData2.pointee.config_len).to(equal(512))
expect(obsoleteHashes).to(equal(["fakehash2"]))
expect(obsoleteHashes).to(equal(["fakehash1"]))
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()

View File

@ -16,9 +16,9 @@ extension DeleteAllBeforeResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { 1 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: UInt64
publicKey: String,
validationData: UInt64,
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = swarm.reduce(into: [:]) { result, next in
guard
@ -40,14 +40,16 @@ extension DeleteAllBeforeResponse: ValidatableResponse {
/// Signature of `( PUBKEY_HEX || BEFORE || DELETEDHASH[0] || ... || DELETEDHASH[N] )`
/// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `DELETEDHASH`
/// values are totally ordered (i.e. among all the hashes deleted regardless of namespace)
let verificationBytes: [UInt8] = userX25519PublicKey.bytes
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: "\(validationData)".data(using: .ascii)?.bytes)
.appending(contentsOf: next.value.deleted.joined().bytes)
result[next.key] = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
result[next.key] = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
}

View File

@ -51,9 +51,9 @@ extension DeleteAllMessagesResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { 1 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: UInt64
publicKey: String,
validationData: UInt64,
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = swarm.reduce(into: [:]) { result, next in
guard
@ -75,14 +75,16 @@ extension DeleteAllMessagesResponse: ValidatableResponse {
/// Signature of `( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )`
/// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `DELETEDHASH`
/// values are totally ordered (i.e. among all the hashes deleted regardless of namespace)
let verificationBytes: [UInt8] = userX25519PublicKey.bytes
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: "\(validationData)".data(using: .ascii)?.bytes)
.appending(contentsOf: next.value.deleted.joined().bytes)
result[next.key] = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
result[next.key] = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
}

View File

@ -38,9 +38,9 @@ extension DeleteMessagesResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { 1 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: [String]
publicKey: String,
validationData: [String],
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = swarm.reduce(into: [:]) { result, next in
guard
@ -60,14 +60,16 @@ extension DeleteMessagesResponse: ValidatableResponse {
}
/// The signature format is `( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )`
let verificationBytes: [UInt8] = userX25519PublicKey.bytes
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: validationData.joined().bytes)
.appending(contentsOf: next.value.deleted.joined().bytes)
result[next.key] = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
result[next.key] = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
}

View File

@ -34,29 +34,43 @@ extension SnodeAPI {
// MARK: - Convenience
func sessionId(sodium: Sodium, nameBytes: [UInt8], nameHashBytes: [UInt8]) throws -> String {
func sessionId(
nameBytes: [UInt8],
nameHashBytes: [UInt8],
using dependencies: Dependencies
) throws -> String {
let ciphertext: [UInt8] = Data(hex: result.encryptedValue).bytes
// Handle old Argon2-based encryption used before HF16
guard let hexEncodedNonce: String = result.nonce else {
let salt: [UInt8] = Data(repeating: 0, count: sodium.pwHash.SaltBytes).bytes
let salt: [UInt8] = Data(
repeating: 0,
count: dependencies[singleton: .crypto].size(.legacyArgon2PWHashSaltBytes)
).bytes
guard
let key: [UInt8] = sodium.pwHash.hash(
outputLength: sodium.secretBox.KeyBytes,
passwd: nameBytes,
salt: salt,
opsLimit: sodium.pwHash.OpsLimitModerate,
memLimit: sodium.pwHash.MemLimitModerate,
alg: .Argon2ID13
let key: [UInt8] = try? dependencies[singleton: .crypto].perform(
.legacyArgon2PWHash(
passwd: nameBytes,
salt: salt
)
)
else { throw SnodeAPIError.hashingFailed }
let nonce: [UInt8] = Data(repeating: 0, count: sodium.secretBox.NonceBytes).bytes
let nonce: [UInt8] = Data(
repeating: 0,
count: dependencies[singleton: .crypto].size(.legacyArgon2SecretBoxNonceBytes)
).bytes
guard let sessionIdAsData: [UInt8] = sodium.secretBox.open(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else {
throw SnodeAPIError.decryptionFailed
}
guard
let sessionIdAsData: [UInt8] = try? dependencies[singleton: .crypto].perform(
.legacyArgon2SecretBoxOpen(
authenticatedCipherText: ciphertext,
secretKey: key,
nonce: nonce
)
)
else { throw SnodeAPIError.decryptionFailed }
return sessionIdAsData.toHexString()
}
@ -65,16 +79,20 @@ extension SnodeAPI {
// xchacha-based encryption
// key = H(name, key=H(name))
guard let key: [UInt8] = sodium.genericHash.hash(message: nameBytes, key: nameHashBytes) else {
throw SnodeAPIError.hashingFailed
}
guard
let key: [UInt8] = try? dependencies[singleton: .crypto].perform(
.hash(message: nameBytes, key: nameHashBytes)
)
else { throw SnodeAPIError.hashingFailed }
guard
// Should always be equal in practice
ciphertext.count >= (SessionId.byteCount + sodium.aead.xchacha20poly1305ietf.ABytes),
let sessionIdAsData = sodium.aead.xchacha20poly1305ietf.decrypt(
authenticatedCipherText: ciphertext,
secretKey: key,
nonce: nonceBytes
ciphertext.count >= (SessionId.byteCount + dependencies[singleton: .crypto].size(.aeadXChaCha20ABytes)),
let sessionIdAsData = try? dependencies[singleton: .crypto].perform(
.decryptAeadXChaCha20(
authenticatedCipherText: ciphertext,
secretKey: key,
nonce: nonceBytes
)
)
else { throw SnodeAPIError.decryptionFailed }

View File

@ -16,9 +16,9 @@ extension RevokeSubkeyResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { -1 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: String
publicKey: String,
validationData: String,
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = try swarm.reduce(into: [:]) { result, next in
guard
@ -37,12 +37,15 @@ extension RevokeSubkeyResponse: ValidatableResponse {
/// Signature of `( PUBKEY_HEX || SUBKEY_TAG_BYTES )` where `SUBKEY_TAG_BYTES` is the
/// requested subkey tag for revocation
let verificationBytes: [UInt8] = userX25519PublicKey.bytes
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: validationData.bytes)
let isValid: Bool = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
let isValid: Bool = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
// If the update signature is invalid then we want to fail here

View File

@ -62,9 +62,9 @@ extension SendMessagesResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { -2 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: Void
publicKey: String,
validationData: Void,
using dependencies: Dependencies
) throws -> [String: Bool] {
let validationMap: [String: Bool] = swarm.reduce(into: [:]) { result, next in
guard
@ -87,10 +87,13 @@ extension SendMessagesResponse: ValidatableResponse {
/// Signature of `hash` signed by the node's ed25519 pubkey
let verificationBytes: [UInt8] = hash.bytes
result[next.key] = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
result[next.key] = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
}

View File

@ -51,9 +51,9 @@ extension UpdateExpiryAllResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { -1 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: UInt64
publicKey: String,
validationData: UInt64,
using dependencies: Dependencies
) throws -> [String: [String]] {
let validationMap: [String: [String]] = try swarm.reduce(into: [:]) { result, next in
guard
@ -75,14 +75,16 @@ extension UpdateExpiryAllResponse: ValidatableResponse {
/// Signature of `( PUBKEY_HEX || EXPIRY || UPDATED[0] || ... || UPDATED[N] )`
/// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `UPDATED`
/// values are totally ordered (i.e. among all the hashes deleted regardless of namespace)
let verificationBytes: [UInt8] = userX25519PublicKey.bytes
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: "\(validationData)".data(using: .ascii)?.bytes)
.appending(contentsOf: next.value.updated.joined().bytes)
let isValid: Bool = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
let isValid: Bool = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
// If the update signature is invalid then we want to fail here

View File

@ -50,9 +50,9 @@ extension UpdateExpiryResponse: ValidatableResponse {
internal static var requiredSuccessfulResponses: Int { -1 }
internal func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: [String]
publicKey: String,
validationData: [String],
using dependencies: Dependencies
) throws -> [String: UpdateExpiryResponseResult] {
let validationMap: [String: UpdateExpiryResponseResult] = try swarm.reduce(into: [:]) { result, next in
guard
@ -80,7 +80,7 @@ extension UpdateExpiryResponse: ValidatableResponse {
///
/// **Note:** If `updated` is empty then the `expiry` value will match the value that was
/// included in the original request
let verificationBytes: [UInt8] = userX25519PublicKey.bytes
let verificationBytes: [UInt8] = publicKey.bytes
.appending(contentsOf: "\(appliedExpiry)".data(using: .ascii)?.bytes)
.appending(contentsOf: validationData.joined().bytes)
.appending(contentsOf: next.value.updated.sorted().joined().bytes)
@ -91,10 +91,12 @@ extension UpdateExpiryResponse: ValidatableResponse {
result.append(contentsOf: "\(nextUnchanged.value)".data(using: .ascii)?.bytes ?? [])
}
)
let isValid: Bool = sodium.sign.verify(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
let isValid: Bool = dependencies[singleton: .crypto].verify(
.signature(
message: verificationBytes,
publicKey: Data(hex: next.key).bytes,
signature: encodedSignature.bytes
)
)
// If the update signature is invalid then we want to fail here

View File

@ -17,7 +17,7 @@ public extension SnodeAPI {
// MARK: - Variables
var publicKey: String {
public var publicKey: String {
switch self {
case .standard(let pubkey, _), .groupAdmin(let pubkey, _), .groupMember(let pubkey, _):
return pubkey
@ -26,7 +26,7 @@ public extension SnodeAPI {
// MARK: - Functions
func generateSignature(
public func generateSignature(
with verificationBytes: [UInt8],
using dependencies: Dependencies
) throws -> [UInt8] {

View File

@ -7,8 +7,6 @@ import GRDB
import SessionUtilitiesKit
public final class SnodeAPI {
internal static let sodium: Atomic<Sodium> = Atomic(Sodium())
private static var hasLoadedSnodePool: Atomic<Bool> = Atomic(false)
private static var loadedSwarms: Atomic<Set<String>> = Atomic([])
private static var getSnodePoolPublisher: Atomic<AnyPublisher<Set<Snode>, Error>?> = Atomic(nil)
@ -330,11 +328,10 @@ public final class SnodeAPI {
.first(where: { $0 is HTTP.BatchSubResponse<UpdateExpiryResponse> })
.asType(HTTP.BatchSubResponse<UpdateExpiryResponse>.self),
let refreshTTLResponse: UpdateExpiryResponse = refreshTTLSubReponse.body,
// TODO: Need to fix this for the group config polling
let validResults: [String: UpdateExpiryResponseResult] = try? refreshTTLResponse.validResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: getUserHexEncodedPublicKey(using: dependencies),
validationData: refreshingConfigHashes
publicKey: authInfo.publicKey,
validationData: refreshingConfigHashes,
using: dependencies
),
let targetResult: UpdateExpiryResponseResult = validResults[snode.ed25519PublicKey],
let groupedExpiryResult: [UInt64: [String]] = targetResult.changed
@ -497,7 +494,7 @@ public final class SnodeAPI {
// Hash the ONS name using BLAKE2b
let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!)
guard let nameHash = sodium.wrappedValue.genericHash.hash(message: nameAsData) else {
guard let nameHash = try? dependencies[singleton: .crypto].perform(.hash(message: nameAsData)) else {
return Fail(error: SnodeAPIError.hashingFailed)
.eraseToAnyPublisher()
}
@ -531,9 +528,9 @@ public final class SnodeAPI {
.decoded(as: ONSResolveResponse.self)
.tryMap { _, response -> String in
try response.sessionId(
sodium: sodium.wrappedValue,
nameBytes: nameAsData,
nameHashBytes: nameHash
nameHashBytes: nameHash,
using: dependencies
)
}
.retry(4)
@ -589,7 +586,6 @@ public final class SnodeAPI {
authInfo: AuthenticationInfo,
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, SendMessagesResponse), Error> {
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
let sendTimestamp: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs(using: dependencies))
// Create a convenience method to send a message to an individual Snode
@ -636,8 +632,8 @@ public final class SnodeAPI {
try sendMessage(to: snode)
.tryMap { info, response -> (ResponseInfoType, SendMessagesResponse) in
try response.validateResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: userX25519PublicKey
publicKey: authInfo.publicKey,
using: dependencies
)
return (info, response)
@ -653,7 +649,6 @@ public final class SnodeAPI {
authInfo: AuthenticationInfo,
using dependencies: Dependencies
) throws -> HTTP.PreparedRequest<SendMessagesResponse> {
let userX25519PublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let request: HTTP.PreparedRequest<SendMessagesResponse> = try {
// Check if this namespace requires authentication
guard namespace.requiresWriteAuthentication else {
@ -688,8 +683,8 @@ public final class SnodeAPI {
return request
.tryMap { _, response -> SendMessagesResponse in
try response.validateResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: userX25519PublicKey
publicKey: authInfo.publicKey,
using: dependencies
)
return response
@ -733,9 +728,9 @@ public final class SnodeAPI {
.decoded(as: UpdateExpiryResponse.self, using: dependencies)
.tryMap { _, response -> [String: UpdateExpiryResponseResult] in
try response.validResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: getUserHexEncodedPublicKey(using: dependencies),
validationData: serverHashes
publicKey: authInfo.publicKey,
validationData: serverHashes,
using: dependencies
)
}
.eraseToAnyPublisher()
@ -765,9 +760,9 @@ public final class SnodeAPI {
.decoded(as: RevokeSubkeyResponse.self, using: dependencies)
.tryMap { _, response -> Void in
try response.validateResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: getUserHexEncodedPublicKey(using: dependencies),
validationData: subkeyToRevoke
publicKey: authInfo.publicKey,
validationData: subkeyToRevoke,
using: dependencies
)
return ()
@ -783,8 +778,6 @@ public final class SnodeAPI {
authInfo: AuthenticationInfo,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: Bool], Error> {
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
return getSwarm(for: authInfo.publicKey, using: dependencies)
.tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in
SnodeAPI
@ -804,9 +797,9 @@ public final class SnodeAPI {
.decoded(as: DeleteMessagesResponse.self, using: dependencies)
.tryMap { _, response -> [String: Bool] in
let validResultMap: [String: Bool] = try response.validResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: userX25519PublicKey,
validationData: serverHashes
publicKey: authInfo.publicKey,
validationData: serverHashes,
using: dependencies
)
// If `validResultMap` didn't throw then at least one service node
@ -832,7 +825,6 @@ public final class SnodeAPI {
authInfo: AuthenticationInfo,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<[String: Bool]> {
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
return try SnodeAPI
.prepareRequest(
request: Request(
@ -848,9 +840,9 @@ public final class SnodeAPI {
)
.tryMap { _, response -> [String: Bool] in
let validResultMap: [String: Bool] = try response.validResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: userX25519PublicKey,
validationData: serverHashes
publicKey: authInfo.publicKey,
validationData: serverHashes,
using: dependencies
)
// If `validResultMap` didn't throw then at least one service node
@ -874,8 +866,6 @@ public final class SnodeAPI {
authInfo: AuthenticationInfo,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: Bool], Error> {
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
return getSwarm(for: authInfo.publicKey, using: dependencies)
.tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in
getNetworkTime(from: snode)
@ -897,9 +887,9 @@ public final class SnodeAPI {
.decoded(as: DeleteAllMessagesResponse.self, using: dependencies)
.tryMap { _, response -> [String: Bool] in
try response.validResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: userX25519PublicKey,
validationData: timestampMs
publicKey: authInfo.publicKey,
validationData: timestampMs,
using: dependencies
)
}
.eraseToAnyPublisher()
@ -915,8 +905,6 @@ public final class SnodeAPI {
authInfo: AuthenticationInfo,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: Bool], Error> {
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
return getSwarm(for: authInfo.publicKey, using: dependencies)
.tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in
getNetworkTime(from: snode)
@ -939,9 +927,9 @@ public final class SnodeAPI {
.decoded(as: DeleteAllBeforeResponse.self, using: dependencies)
.tryMap { _, response -> [String: Bool] in
try response.validResultMap(
sodium: sodium.wrappedValue,
userX25519PublicKey: userX25519PublicKey,
validationData: beforeMs
publicKey: authInfo.publicKey,
validationData: beforeMs,
using: dependencies
)
}
.eraseToAnyPublisher()

View File

@ -2,6 +2,7 @@
import Foundation
import Sodium
import SessionUtilitiesKit
internal protocol ValidatableResponse {
associatedtype ValidationData
@ -21,22 +22,30 @@ internal protocol ValidatableResponse {
) throws -> [String: ValidationResponse]
func validResultMap(
sodium: Sodium,
userX25519PublicKey: String,
validationData: ValidationData
publicKey: String,
validationData: ValidationData,
using dependencies: Dependencies
) throws -> [String: ValidationResponse]
func validateResultMap(sodium: Sodium, userX25519PublicKey: String, validationData: ValidationData) throws
func validateResultMap(
publicKey: String,
validationData: ValidationData,
using dependencies: Dependencies
) throws
}
// MARK: - Convenience
internal extension ValidatableResponse {
func validateResultMap(sodium: Sodium, userX25519PublicKey: String, validationData: ValidationData) throws {
func validateResultMap(
publicKey: String,
validationData: ValidationData,
using dependencies: Dependencies
) throws {
_ = try validResultMap(
sodium: sodium,
userX25519PublicKey: userX25519PublicKey,
validationData: validationData
publicKey: publicKey,
validationData: validationData,
using: dependencies
)
}
@ -63,15 +72,21 @@ internal extension ValidatableResponse {
}
internal extension ValidatableResponse where ValidationData == Void {
func validResultMap(sodium: Sodium, userX25519PublicKey: String) throws -> [String: ValidationResponse] {
return try validResultMap(sodium: sodium, userX25519PublicKey: userX25519PublicKey, validationData: ())
func validResultMap(
publicKey: String,
using dependencies: Dependencies
) throws -> [String: ValidationResponse] {
return try validResultMap(publicKey: publicKey, validationData: (), using: dependencies)
}
func validateResultMap(sodium: Sodium, userX25519PublicKey: String) throws {
func validateResultMap(
publicKey: String,
using dependencies: Dependencies
) throws {
_ = try validResultMap(
sodium: sodium,
userX25519PublicKey: userX25519PublicKey,
validationData: ()
publicKey: publicKey,
validationData: (),
using: dependencies
)
}
}

View File

@ -2,22 +2,139 @@
import Foundation
import Sodium
import Clibsodium
import SessionUtilitiesKit
// MARK: - Generic Hash
public extension Crypto.Action {
static func hash(message: Bytes, key: Bytes? = nil) -> Crypto.Action {
return Crypto.Action(id: "hash", args: [message, key]) { sodium in
sodium.genericHash.hash(message: message, key: key)
}
}
}
// MARK: - Sign
public extension Crypto.Action {
static func signature(message: Bytes, secretKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "signature", args: [message, secretKey]) {
Sodium().sign.signature(message: message, secretKey: secretKey)
return Crypto.Action(id: "signature", args: [message, secretKey]) { sodium in
sodium.sign.signature(message: message, secretKey: secretKey)
}
}
}
public extension Crypto.Verification {
static func signature(message: Bytes, publicKey: Bytes, signature: Bytes) -> Crypto.Verification {
return Crypto.Verification(id: "signature", args: [message, publicKey, signature]) {
Sodium().sign.verify(message: message, publicKey: publicKey, signature: signature)
return Crypto.Verification(id: "signature", args: [message, publicKey, signature]) { sodium in
sodium.sign.verify(message: message, publicKey: publicKey, signature: signature)
}
}
}
// MARK: - AeadXChaCha20Poly1305Ietf
public extension Crypto.Size {
static let aeadXChaCha20KeyBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20KeyBytes") { sodium in
sodium.aead.xchacha20poly1305ietf.KeyBytes
}
static let aeadXChaCha20ABytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20ABytes") { sodium in
sodium.aead.xchacha20poly1305ietf.ABytes
}
}
public extension Crypto.Action {
/// This method is the same as the standard AeadXChaCha20Poly1305Ietf `encrypt` method except it allows the
/// specification of a nonce which allows for deterministic behaviour with unit testing
static func encryptAeadXChaCha20(
message: Bytes,
secretKey: Bytes,
nonce: Bytes,
additionalData: Bytes? = nil,
using dependencies: Dependencies
) -> Crypto.Action {
return Crypto.Action(
id: "encryptAeadXChaCha20",
args: [message, secretKey, nonce, additionalData]
) {
guard secretKey.count == dependencies[singleton: .crypto].size(.aeadXChaCha20KeyBytes) else { return nil }
var authenticatedCipherText = Bytes(
repeating: 0,
count: message.count + dependencies[singleton: .crypto].size(.aeadXChaCha20ABytes)
)
var authenticatedCipherTextLen: UInt64 = 0
let result = crypto_aead_xchacha20poly1305_ietf_encrypt(
&authenticatedCipherText, &authenticatedCipherTextLen,
message, UInt64(message.count),
additionalData, UInt64(additionalData?.count ?? 0),
nil, nonce, secretKey
)
guard result == 0 else { return nil }
return authenticatedCipherText
}
}
static func decryptAeadXChaCha20(
authenticatedCipherText: Bytes,
secretKey: Bytes,
nonce: Bytes,
additionalData: Bytes? = nil
) -> Crypto.Action {
return Crypto.Action(
id: "decryptAeadXChaCha20",
args: [authenticatedCipherText, secretKey, nonce, additionalData]
) { sodium in
sodium.aead.xchacha20poly1305ietf.decrypt(
authenticatedCipherText: authenticatedCipherText,
secretKey: secretKey,
nonce: nonce,
additionalData: additionalData
)
}
}
}
// MARK: - Legacy Argon2-based encryption
public extension Crypto.Size {
static let legacyArgon2PWHashSaltBytes: Crypto.Size = Crypto.Size(id: "legacyArgon2PWHashSaltBytes") {
$0.pwHash.SaltBytes
}
static let legacyArgon2SecretBoxNonceBytes: Crypto.Size = Crypto.Size(id: "legacyArgon2SecretBoxNonceBytes") {
$0.secretBox.NonceBytes
}
}
public extension Crypto.Action {
static func legacyArgon2PWHash(passwd: Bytes, salt: Bytes) -> Crypto.Action {
return Crypto.Action(id: "legacyArgon2PWHash", args: [passwd, salt]) { sodium in
sodium.pwHash.hash(
outputLength: sodium.secretBox.KeyBytes,
passwd: passwd,
salt: salt,
opsLimit: sodium.pwHash.OpsLimitModerate,
memLimit: sodium.pwHash.MemLimitModerate,
alg: .Argon2ID13
)
}
}
static func legacyArgon2SecretBoxOpen(
authenticatedCipherText: Bytes,
secretKey: Bytes,
nonce: Bytes
) -> Crypto.Action {
return Crypto.Action(id: "legacyArgon2SecretBoxOpen", args: [authenticatedCipherText, secretKey, nonce]) {
$0.secretBox.open(
authenticatedCipherText: authenticatedCipherText,
secretKey: secretKey,
nonce: nonce
)
}
}
}

View File

@ -0,0 +1,84 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Clibsodium
import Curve25519Kit
// MARK: - Box
public extension Crypto.Size {
static let publicKey: Crypto.Size = Crypto.Size(id: "publicKey") { $0.sign.PublicKeyBytes }
static let secretKey: Crypto.Size = Crypto.Size(id: "secretKey") { $0.sign.SecretKeyBytes }
}
// MARK: - Sign
public extension Crypto.Action {
static func toX25519(ed25519PublicKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "toX25519", args: [ed25519PublicKey]) { sodium in
sodium.sign.toX25519(ed25519PublicKey: ed25519PublicKey)
}
}
static func toX25519(ed25519SecretKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "toX25519", args: [ed25519SecretKey]) { sodium in
sodium.sign.toX25519(ed25519SecretKey: ed25519SecretKey)
}
}
}
// MARK: - Ed25519
public extension Crypto.KeyPairType {
static func x25519KeyPair() -> Crypto.KeyPairType {
return Crypto.KeyPairType(id: "x25519KeyPair") {
let keyPair: ECKeyPair = Curve25519.generateKeyPair()
return KeyPair(publicKey: Array(keyPair.publicKey), secretKey: Array(keyPair.privateKey))
}
}
static func ed25519KeyPair(
seed: Data? = nil,
using dependencies: Dependencies = Dependencies()
) -> Crypto.KeyPairType {
return Crypto.KeyPairType(id: "ed25519KeyPair") {
let pkSize: Int = dependencies[singleton: .crypto].size(.publicKey)
let skSize: Int = dependencies[singleton: .crypto].size(.secretKey)
var edPK: [UInt8] = [UInt8](repeating: 0, count: pkSize)
var edSK: [UInt8] = [UInt8](repeating: 0, count: skSize)
var targetSeed: [UInt8] = ((seed ?? (try? Randomness.generateRandomBytes(numberBytes: skSize)))
.map { Array($0) })
.defaulting(to: [])
// Generate the key
guard Sodium.lib_crypto_sign_ed25519_seed_keypair(&edPK, &edSK, &targetSeed) == 0 else {
return nil
}
return KeyPair(publicKey: edPK, secretKey: edSK)
}
}
}
public extension Crypto.Action {
static func signEd25519(data: Bytes, keyPair: KeyPair) -> Crypto.Action {
return Crypto.Action(id: "signEd25519", args: [data, keyPair]) {
let ecKeyPair: ECKeyPair = try ECKeyPair(
publicKeyData: Data(keyPair.publicKey),
privateKeyData: Data(keyPair.secretKey)
)
return try Ed25519.sign(Data(data), with: ecKeyPair).bytes
}
}
}
public extension Crypto.Verification {
static func signatureEd25519(_ signature: Data, publicKey: Data, data: Data) -> Crypto.Verification {
return Crypto.Verification(id: "signatureEd25519", args: [signature, publicKey, data]) {
return ((try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true)
}
}
}

View File

@ -38,63 +38,95 @@ public enum CryptoError: LocalizedError {
// MARK: - Crypto
public struct Crypto: CryptoType {
private let sodium: Sodium = Sodium()
public struct Size {
public let id: String
public let args: [Any?]
let get: () -> Int
let get: (Sodium) -> Int
public init(id: String, args: [Any?] = [], get: @escaping (Sodium) -> Int) {
self.id = id
self.args = args
self.get = get
}
public init(id: String, args: [Any?] = [], get: @escaping () -> Int) {
self.id = id
self.args = args
self.get = get
self.get = { _ in get() }
}
}
public struct Action {
public let id: String
public let args: [Any?]
let perform: () throws -> Array<UInt8>
let perform: (Sodium) throws -> Array<UInt8>
public init(id: String, args: [Any?] = [], perform: @escaping () throws -> Array<UInt8>) {
public init(id: String, args: [Any?] = [], perform: @escaping (Sodium) throws -> Array<UInt8>) {
self.id = id
self.args = args
self.perform = perform
}
public init(id: String, args: [Any?] = [], perform: @escaping () throws -> Array<UInt8>) {
self.id = id
self.args = args
self.perform = { _ in try perform() }
}
public init(id: String, args: [Any?] = [], perform: @escaping (Sodium) -> Array<UInt8>?) {
self.id = id
self.args = args
self.perform = { try perform($0) ?? { throw CryptoError.failedToGenerateOutput }() }
}
public init(id: String, args: [Any?] = [], perform: @escaping () -> Array<UInt8>?) {
self.id = id
self.args = args
self.perform = { try perform() ?? { throw CryptoError.failedToGenerateOutput }() }
self.perform = { _ in try perform() ?? { throw CryptoError.failedToGenerateOutput }() }
}
}
public struct Verification {
public let id: String
public let args: [Any?]
let verify: () -> Bool
let verify: (Sodium) -> Bool
public init(id: String, args: [Any?] = [], verify: @escaping (Sodium) -> Bool) {
self.id = id
self.args = args
self.verify = verify
}
public init(id: String, args: [Any?] = [], verify: @escaping () -> Bool) {
self.id = id
self.args = args
self.verify = verify
self.verify = { _ in verify() }
}
}
public struct KeyPairType {
public let id: String
public let args: [Any?]
let generate: () -> KeyPair?
let generate: (Sodium) -> KeyPair?
public init(id: String, args: [Any?] = [], generate: @escaping () -> KeyPair?) {
public init(id: String, args: [Any?] = [], generate: @escaping (Sodium) -> KeyPair?) {
self.id = id
self.args = args
self.generate = generate
}
public init(id: String, args: [Any?] = [], generate: @escaping () -> KeyPair?) {
self.id = id
self.args = args
self.generate = { _ in generate() }
}
}
public init() {}
public func size(_ size: Crypto.Size) -> Int { return size.get() }
public func perform(_ action: Crypto.Action) throws -> Array<UInt8> { return try action.perform() }
public func verify(_ verification: Crypto.Verification) -> Bool { return verification.verify() }
public func generate(_ keyPairType: Crypto.KeyPairType) -> KeyPair? { return keyPairType.generate() }
public func size(_ size: Crypto.Size) -> Int { return size.get(sodium) }
public func perform(_ action: Crypto.Action) throws -> Array<UInt8> { return try action.perform(sodium) }
public func verify(_ verification: Crypto.Verification) -> Bool { return verification.verify(sodium) }
public func generate(_ keyPairType: Crypto.KeyPairType) -> KeyPair? { return keyPairType.generate(sodium) }
}

View File

@ -2,8 +2,6 @@
import Foundation
import GRDB
import Sodium
import Curve25519Kit
public struct Identity: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "identity" }
@ -41,18 +39,25 @@ public struct Identity: Codable, Identifiable, FetchableRecord, PersistableRecor
// MARK: - GRDB Interactions
public extension Identity {
static func generate(from seed: Data) throws -> (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) {
static func generate(
from seed: Data,
using dependencies: Dependencies = Dependencies()
) throws -> (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) {
guard (seed.count == 16) else { throw GeneralError.invalidSeed }
let padding = Data(repeating: 0, count: 16)
guard
let ed25519KeyPair = Sodium().sign.keyPair(seed: (seed + padding).bytes),
let x25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: ed25519KeyPair.publicKey),
let x25519SecretKey = Sodium().sign.toX25519(ed25519SecretKey: ed25519KeyPair.secretKey)
else {
throw GeneralError.keyGenerationFailed
}
let ed25519KeyPair: KeyPair = dependencies[singleton: .crypto].generate(
.ed25519KeyPair(seed: (seed + padding), using: dependencies)
),
let x25519PublicKey: [UInt8] = try? dependencies[singleton: .crypto].perform(
.toX25519(ed25519PublicKey: ed25519KeyPair.publicKey)
),
let x25519SecretKey: [UInt8] = try? dependencies[singleton: .crypto].perform(
.toX25519(ed25519PublicKey: ed25519KeyPair.secretKey)
)
else { throw GeneralError.keyGenerationFailed }
return (
ed25519KeyPair: KeyPair(

View File

@ -91,7 +91,7 @@ public extension HTTP {
// Results are returned in the same order they were made in so we can use the matching
// indexes to get the correct response
return { info, response in
let convertedResponse: Any? = try? {
let convertedResponse: Any = try {
switch response {
case let batchResponse as HTTP.BatchResponse:
return HTTP.BatchResponse(
@ -318,20 +318,18 @@ public protocol ErasedPreparedRequest {
extension HTTP.PreparedRequest: ErasedPreparedRequest {
public var erasedResponseConverter: ((ResponseInfoType, Any) throws -> Any) {
let originalType: Decodable.Type = self.originalType
let responseConverter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter
let converter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter
return { info, data in
switch data {
case let erasedSubResponse as ErasedBatchSubResponse:
case let subResponse as ErasedBatchSubResponse:
return HTTP.BatchSubResponse(
code: erasedSubResponse.code,
headers: erasedSubResponse.headers,
body: try erasedSubResponse.erasedBody
.map { originalType.from($0) }
.map { try responseConverter(info, $0) }
code: subResponse.code,
headers: subResponse.headers,
body: try originalType.from(subResponse.erasedBody).map { try converter(info, $0) }
)
default: return try originalType.from(data).map { try responseConverter(info, $0) } as Any
default: return try originalType.from(data).map { try converter(info, $0) } as Any
}
}
}
@ -346,15 +344,15 @@ extension HTTP.PreparedRequest: ErasedPreparedRequest {
return { info, _, data in
switch data {
case let erasedSubResponse as ErasedBatchSubResponse:
case let subResponse as ErasedBatchSubResponse:
guard
let erasedBody: Any = erasedSubResponse.erasedBody.map({ originalType.from($0) }),
let erasedBody: Any = originalType.from(subResponse.erasedBody),
let validResponse: R = try? originalConverter(info, erasedBody)
else { return }
outputEventHandler(CachedResponse(
info: info,
originalData: erasedSubResponse.erasedBody as Any,
originalData: subResponse.erasedBody as Any,
convertedData: validResponse
))
@ -653,7 +651,7 @@ public extension Publisher where Failure == Error {
// MARK: - Decoding
public extension Decodable {
fileprivate static func from(_ value: Any) -> Self? {
fileprivate static func from(_ value: Any?) -> Self? {
return (value as? Self)
}