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:
parent
b31afa89e1
commit
0982526057
|
@ -1 +1 @@
|
|||
Subproject commit bb7a2cfa1bbbfe94db7a69ffc4875cdf48330432
|
||||
Subproject commit e21302b598b5dde44fd72566d8b89d4d41cbb9ce
|
|
@ -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 */,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ enum MockDataGenerator {
|
|||
threadId: randomGroupPublicKey,
|
||||
name: groupName,
|
||||
formationTimestamp: timestampNow,
|
||||
approved: true
|
||||
invited: false
|
||||
)
|
||||
.saved(db)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue