Additional encryption work on id blinding

Got the updated blinding logic working (at least when authenticating a request - still need to deal with message signing and verification)
Storing the server capabilities in the database now so we can correctly blind requests based on them
Renamed the remaining 'v2' functions and classes to just be 'OpenGroup' since there isn't a 'V2' anymore
Cleaned up a few TODOs and functions
This commit is contained in:
Morgan Pretty 2022-02-17 18:33:23 +11:00
parent b655882cbd
commit ef09d4d5aa
40 changed files with 631 additions and 267 deletions

View File

@ -8,7 +8,8 @@ inhibit_all_warnings!
abstract_target 'GlobalDependencies' do
pod 'PromiseKit'
pod 'CryptoSwift'
pod 'Sodium', '~> 0.9.1'
# FIXME: If https://github.com/jedisct1/swift-sodium/pull/249 gets resolved then revert this back to the standard pod
pod 'Sodium', :git => 'https://github.com/mpretty-cyro/swift-sodium.git', branch: 'full-clibsodium-build'
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/oxen-io/session-ios-yap-database.git', branch: 'signal-release'
target 'Session' do

View File

@ -132,7 +132,7 @@ DEPENDENCIES:
- Reachability
- SAMKeychain
- SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, branch `session-version`)
- Sodium (~> 0.9.1)
- Sodium (from `https://github.com/mpretty-cyro/swift-sodium.git`, branch `full-clibsodium-build`)
- SwiftProtobuf (~> 1.5.0)
- YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`)
- YYImage (from `https://github.com/signalapp/YYImage`)
@ -150,7 +150,6 @@ SPEC REPOS:
- PureLayout
- Reachability
- SAMKeychain
- Sodium
- SQLCipher
- SwiftProtobuf
- ZXingObjC
@ -164,6 +163,9 @@ EXTERNAL SOURCES:
SignalCoreKit:
:branch: session-version
:git: https://github.com/oxen-io/session-ios-core-kit
Sodium:
:branch: full-clibsodium-build
:git: https://github.com/mpretty-cyro/swift-sodium.git
YapDatabase:
:branch: signal-release
:git: https://github.com/oxen-io/session-ios-yap-database.git
@ -180,6 +182,9 @@ CHECKOUT OPTIONS:
SignalCoreKit:
:commit: 4590c2737a2b5dc0ef4ace9f9019b581caccc1de
:git: https://github.com/oxen-io/session-ios-core-kit
Sodium:
:commit: eeb18f6fa8c28dc254e64cb340ee2c8f37f2ebe8
:git: https://github.com/mpretty-cyro/swift-sodium.git
YapDatabase:
:commit: d84069e25e12a16ab4422e5258127a04b70489ad
:git: https://github.com/oxen-io/session-ios-yap-database.git
@ -201,13 +206,13 @@ SPEC CHECKSUMS:
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d
Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da
Sodium: a7d42cb46e789d2630fa552d35870b416ed055ae
SQLCipher: 98dc22f27c0b1790d39e710d440f22a466ebdb59
SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: a4acbe047a767c48a709e93318532fbf345330dd
PODFILE CHECKSUM: 011a84301a09b80dfc7343c105fc810eacb31074
COCOAPODS: 1.11.2

View File

@ -807,7 +807,7 @@
FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */; };
FDC4384927B47F4D00C60D73 /* LegacyCompactPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */; };
FDC4384A27B47F4D00C60D73 /* LegacyDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384627B47F4D00C60D73 /* LegacyDeletion.swift */; };
FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */; };
FDC4384C27B47F7700C60D73 /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroup.swift */; };
FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; };
FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; };
FDC4385727B484B700C60D73 /* LegacyFileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385627B484B700C60D73 /* LegacyFileUploadResponse.swift */; };
@ -848,6 +848,7 @@
FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; };
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; };
FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CC27BC641200C60D73 /* Set+Utilities.swift */; };
FDC438CF27BCA45400C60D73 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CE27BCA45400C60D73 /* Server.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -1943,7 +1944,7 @@
FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyRoomInfo.swift; sourceTree = "<group>"; };
FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollResponse.swift; sourceTree = "<group>"; };
FDC4384627B47F4D00C60D73 /* LegacyDeletion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyDeletion.swift; sourceTree = "<group>"; };
FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupV2.swift; sourceTree = "<group>"; };
FDC4384B27B47F7700C60D73 /* OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = "<group>"; };
FDC4384E27B4804F00C60D73 /* Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Header.swift; sourceTree = "<group>"; };
FDC4385027B4807400C60D73 /* QueryParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParam.swift; sourceTree = "<group>"; };
FDC4385627B484B700C60D73 /* LegacyFileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFileUploadResponse.swift; sourceTree = "<group>"; };
@ -1982,6 +1983,7 @@
FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequest.swift; sourceTree = "<group>"; };
FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = "<group>"; };
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = "<group>"; };
FDC438CE27BCA45400C60D73 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -3840,7 +3842,8 @@
FDC4381827B34EAD00C60D73 /* Models */ = {
isa = PBXGroup;
children = (
FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */,
FDC438CE27BCA45400C60D73 /* Server.swift */,
FDC4384B27B47F7700C60D73 /* OpenGroup.swift */,
FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */,
FDC4386627B4E10E00C60D73 /* Capabilities.swift */,
FDC4385C27B4C18900C60D73 /* Room.swift */,
@ -5129,6 +5132,7 @@
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */,
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
FDC438CF27BCA45400C60D73 /* Server.swift in Sources */,
FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */,
C32C59C1256DB41F003C73A2 /* TSGroupThread.m in Sources */,
C3A3A08F256E1728004D228D /* FullTextSearchFinder.swift in Sources */,
@ -5211,7 +5215,7 @@
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */,
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */,
FDC4384C27B47F7700C60D73 /* OpenGroup.swift in Sources */,
FDC4382627B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift in Sources */,
B88FA7B826045D100049422F /* OpenGroupAPI.swift in Sources */,
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,

View File

@ -699,8 +699,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let threadID = thread.uniqueId!
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
let publicKey = message.authorId
guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID) else { return }
OpenGroupAPI.legacyBan(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
guard let openGroup = Storage.shared.getOpenGroup(for: threadID) else { return }
OpenGroupAPI.legacyBan(publicKey, from: openGroup.room, on: openGroup.server).retainUntilComplete()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
@ -713,8 +713,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let threadID = thread.uniqueId!
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
let publicKey = message.authorId
guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID) else { return }
OpenGroupAPI.legacyBanAndDeleteAllMessages(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
guard let openGroup = Storage.shared.getOpenGroup(for: threadID) else { return }
OpenGroupAPI.legacyBanAndDeleteAllMessages(publicKey, from: openGroup.room, on: openGroup.server).retainUntilComplete()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
present(alert, animated: true, completion: nil)

View File

@ -370,11 +370,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
if !draft.isEmpty {
snInputView.text = draft
}
// Update member count if this is a V2 open group
// TODO: Non-legacy version (I assue this comes through room updates... 'activeUsers'?
if let v2OpenGroup = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
OpenGroupAPI.legacyGetMemberCount(for: v2OpenGroup.room, on: v2OpenGroup.server).retainUntilComplete()
}
}
override func viewDidLayoutSubviews() {

View File

@ -1000,17 +1000,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!message.isOpenGroupMessage) return;
// Get the open group
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
if (openGroupV2 == nil) return;
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil) return;
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
if (![SNOpenGroupManager isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; }
if (![SNOpenGroupManager isUserModerator:userPublicKey forRoom:openGroup.room onServer:openGroup.server]) { return; }
}
// Delete the message
[[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroupV2.room onServer:openGroupV2.server].catch(^(NSError *error) {
[[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroup.room onServer:openGroup.server].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
@ -1053,21 +1053,21 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!message.isOpenGroupMessage) return;
// Get the open group
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil && openGroupV2 == nil) return;
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil && openGroup == nil) return;
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
if (openGroupV2 != nil) {
if (![SNOpenGroupManager isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; }
if (openGroup != nil) {
if (![SNOpenGroupManager isUserModerator:userPublicKey forRoom:openGroup.room onServer:openGroup.server]) { return; }
}
}
// Delete the message
BOOL wasSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
if (openGroupV2 != nil) {
[[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroupV2.room onServer:openGroupV2.server].catch(^(NSError *error) {
if (openGroup != nil) {
[[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroup.room onServer:openGroup.server].catch(^(NSError *error) {
// Roll back
[self.interaction save];
}) retainUntilComplete];
@ -1127,13 +1127,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!message.isOpenGroupMessage) return true;
// Ensure we have the details needed to contact the server
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
if (openGroupV2 == nil) return true;
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil) return true;
if (interationType == OWSInteractionType_IncomingMessage) {
// Only allow deletion on incoming messages if the user has moderation permission
if (openGroupV2 != nil) {
return [SNOpenGroupManager isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server];
if (openGroup != nil) {
return [SNOpenGroupManager isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroup.room onServer:openGroup.server];
}
} else {
return YES;
@ -1150,12 +1150,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!message.isOpenGroupMessage) return false;
// Ensure we have the details needed to contact the server
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
if (openGroupV2 == nil) return false;
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
if (openGroup == nil) return false;
// Check that we're a moderator
if (openGroupV2 != nil) {
return [SNOpenGroupManager isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server];
if (openGroup != nil) {
return [SNOpenGroupManager isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroup.room onServer:openGroup.server];
}
}

View File

@ -319,9 +319,9 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
}
func showMentionsUI(for candidates: [Mention], in thread: TSThread) {
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
mentionsView.openGroupServer = openGroupV2.server
mentionsView.openGroupRoom = openGroupV2.room
if let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!) {
mentionsView.openGroupServer = openGroup.server
mentionsView.openGroupRoom = openGroup.room
}
mentionsView.candidates = candidates
let mentionCellHeight = Values.smallProfilePictureSize + 2 * Values.smallSpacing

View File

@ -218,8 +218,8 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
profilePictureView.update(for: senderSessionID)
}
if let senderSessionID = senderSessionID, message.isOpenGroupMessage {
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: message.uniqueThreadId) {
let isUserModerator = OpenGroupManager.isUserModerator(senderSessionID, for: openGroupV2.room, on: openGroupV2.server)
if let openGroup = Storage.shared.getOpenGroup(for: message.uniqueThreadId) {
let isUserModerator = OpenGroupManager.isUserModerator(senderSessionID, for: openGroup.room, on: openGroup.server)
moderatorIconImageView.isHidden = !isUserModerator || profilePictureView.isHidden
} else {
moderatorIconImageView.isHidden = true

View File

@ -946,7 +946,7 @@ CGFloat kIconViewLength = 24;
- (void)inviteUsersToOpenGroup
{
NSString *threadID = self.thread.uniqueId;
SNOpenGroupV2 *openGroup = [LKStorage.shared getV2OpenGroupForThreadID:threadID];
SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:threadID];
NSString *url = [NSString stringWithFormat:@"%@/%@?public_key=%@", openGroup.server, openGroup.room, openGroup.publicKey];
SNUserSelectionVC *userSelectionVC = [[SNUserSelectionVC alloc] initWithTitle:NSLocalizedString(@"vc_conversation_settings_invite_button_title", @"")
excluding:[NSSet new]

View File

@ -106,8 +106,8 @@ final class ConversationTitleView : UIView {
switch thread.groupModel.groupType {
case .closedGroup: userCount = UInt64(thread.groupModel.groupMemberIds.count)
case .openGroup:
guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: self.thread.uniqueId!) else { return nil }
userCount = Storage.shared.getUserCount(forV2OpenGroupWithID: openGroupV2.id)
guard let openGroup = Storage.shared.getOpenGroup(for: self.thread.uniqueId!) else { return nil }
userCount = Storage.shared.getUserCount(forOpenGroupWithID: openGroup.id)
default: break
}
if let userCount = userCount {

View File

@ -521,11 +521,11 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
private func delete(_ thread: TSThread) {
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)
let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)
Storage.write { transaction in
Storage.shared.cancelPendingMessageSendJobs(for: thread.uniqueId!, using: transaction)
if let openGroupV2 = openGroupV2 {
OpenGroupManager.shared.delete(openGroupV2, associatedWith: thread, using: transaction)
if let openGroup = openGroup {
OpenGroupManager.shared.delete(openGroup, associatedWith: thread, using: transaction)
} else if let thread = thread as? TSGroupThread, thread.isClosedGroup == true {
let groupID = thread.groupModel.groupId
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)

View File

@ -14,7 +14,7 @@ public final class BackgroundPoller: NSObject {
.appending(pollForMessages())
.appending(pollForClosedGroupMessages())
.appending(
Set(Storage.shared.getAllV2OpenGroups().values.map { $0.server })
Set(Storage.shared.getAllOpenGroups().values.map { $0.server })
.map { server in
let poller = OpenGroupAPI.Poller(for: server)
poller.stop()

View File

@ -9,7 +9,7 @@ public final class MentionUtilities : NSObject {
}
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
let openGroup = Storage.shared.getOpenGroup(for: threadID)
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID, in: transaction)
}
@ -22,7 +22,7 @@ public final class MentionUtilities : NSObject {
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
let matchEnd: Int
if knownPublicKeys.contains(publicKey) {
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
let context: Contact.Context = (openGroup != nil) ? .openGroup : .regular
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
if let displayName = displayName {
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")

View File

@ -12,7 +12,7 @@ enum Header: String {
case sogsPubKey = "X-SOGS-Pubkey"
case sogsNonce = "X-SOGS-Nonce"
case sogsTimestamp = "X-SOGS-Timestamp"
case sogsHash = "X-SOGS-Hash"
case sogsSignature = "X-SOGS-Signature"
}
// MARK: - Convenience

View File

@ -7,7 +7,7 @@ extension Storage {
let transaction = transaction as! YapDatabaseReadWriteTransaction
var threadOrNil: TSThread?
if let openGroupID = openGroupID {
if let threadID = Storage.shared.v2GetThreadID(for: openGroupID),
if let threadID = Storage.shared.getThreadID(for: openGroupID),
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) {
threadOrNil = thread
}

View File

@ -1,46 +1,35 @@
public protocol SessionMessagingKitOpenGroupStorageProtocol {
func getOpenGroupImage(for room: String, on server: String) -> Data?
func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any)
func getV2OpenGroup(for threadID: String) -> OpenGroupV2?
func setV2OpenGroup(_ openGroup: OpenGroupV2, for threadID: String, using transaction: Any)
func getUserCount(forV2OpenGroupWithID openGroupID: String) -> UInt64?
func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any)
}
extension Storage: SessionMessagingKitOpenGroupStorageProtocol {
extension Storage {
// MARK: - Open Groups
private static let openGroupCollection = "SNOpenGroupCollection"
@objc public func getAllV2OpenGroups() -> [String:OpenGroupV2] {
var result = [String:OpenGroupV2]()
@objc public func getAllOpenGroups() -> [String: OpenGroup] {
var result = [String: OpenGroup]()
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: Storage.openGroupCollection) { threadID, object, _ in
guard let openGroup = object as? OpenGroupV2 else { return }
guard let openGroup = object as? OpenGroup else { return }
result[threadID] = openGroup
}
}
return result
}
@objc(getV2OpenGroupForThreadID:)
public func getV2OpenGroup(for threadID: String) -> OpenGroupV2? {
var result: OpenGroupV2?
@objc(getOpenGroupForThreadID:)
public func getOpenGroup(for threadID: String) -> OpenGroup? {
var result: OpenGroup?
Storage.read { transaction in
result = transaction.object(forKey: threadID, inCollection: Storage.openGroupCollection) as? OpenGroupV2
result = transaction.object(forKey: threadID, inCollection: Storage.openGroupCollection) as? OpenGroup
}
return result
}
public func v2GetThreadID(for v2OpenGroupID: String) -> String? {
public func getThreadID(for openGroupID: String) -> String? {
var result: String?
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: Storage.openGroupCollection, using: { threadID, object, stop in
guard let openGroup = object as? OpenGroupV2, openGroup.id == v2OpenGroupID else { return }
guard let openGroup = object as? OpenGroup, openGroup.id == openGroupID else { return }
result = threadID
stop.pointee = true
})
@ -48,17 +37,27 @@ extension Storage: SessionMessagingKitOpenGroupStorageProtocol {
return result
}
@objc(setV2OpenGroup:forThreadWithID:using:)
public func setV2OpenGroup(_ openGroup: OpenGroupV2, for threadID: String, using transaction: Any) {
@objc(setOpenGroup:forThreadWithID:using:)
public func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(openGroup, forKey: threadID, inCollection: Storage.openGroupCollection)
}
@objc(removeV2OpenGroupForThreadID:using:)
public func removeV2OpenGroup(for threadID: String, using transaction: Any) {
@objc(removeOpenGroupForThreadID:using:)
public func removeOpenGroup(for threadID: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: threadID, inCollection: Storage.openGroupCollection)
}
public func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? {
var result: OpenGroupAPI.Server?
Storage.read { transaction in
result = transaction.object(forKey: "SOGS.\(name)", inCollection: Storage.openGroupCollection) as? OpenGroupAPI.Server
}
return result
}
public func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(server, forKey: "SOGS.\(server.name)", inCollection: Storage.openGroupCollection)
}
// MARK: - Authorization
@ -171,7 +170,7 @@ extension Storage: SessionMessagingKitOpenGroupStorageProtocol {
private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"
private static let openGroupImageCollection = "SNOpenGroupImageCollection"
public func getUserCount(forV2OpenGroupWithID openGroupID: String) -> UInt64? {
public func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? {
var result: UInt64?
Storage.read { transaction in
result = transaction.object(forKey: openGroupID, inCollection: Storage.openGroupUserCountCollection) as? UInt64
@ -179,7 +178,7 @@ extension Storage: SessionMessagingKitOpenGroupStorageProtocol {
return result
}
public func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) {
public func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: openGroupID, inCollection: Storage.openGroupUserCountCollection)
}

View File

@ -96,7 +96,11 @@ public final class FileServerAPIV2 : NSObject {
// TODO: Upgrade this to use the V4 onion requests once supported.
return OnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: .v3, with: serverPublicKey)
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
.map2 { _, response in
guard let response: Data = response else { throw Error.parsingFailed }
return response
}
}
// MARK: File Storage

View File

@ -97,12 +97,12 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
self.handleFailure(error: error)
}
}
if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let v2OpenGroup = storage.getV2OpenGroup(for: tsMessage.uniqueThreadId) {
if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let openGroup = storage.getOpenGroup(for: tsMessage.uniqueThreadId) {
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
return handleFailure(Error.invalidURL)
}
// TODO: Upgrade this to use the non-legacy version
OpenGroupAPI.legacyDownload(file, from: v2OpenGroup.room, on: v2OpenGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
OpenGroupAPI.legacyDownload(file, from: openGroup.room, on: openGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
}.catch(on: DispatchQueue.global()) { error in
handleFailure(error)

View File

@ -68,12 +68,12 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
}
guard !stream.isUploaded else { return handleSuccess() } // Should never occur
let storage = SNMessagingKitConfiguration.shared.storage
if let v2OpenGroup = storage.getV2OpenGroup(for: threadID) {
if let openGroup = storage.getOpenGroup(for: threadID) {
AttachmentUploadJob.upload(
stream,
using: { data in
// TODO: Upgrade this to use the non-legacy version
return OpenGroupAPI.legacyUpload(data, to: v2OpenGroup.room, on: v2OpenGroup.server)
return OpenGroupAPI.legacyUpload(data, to: openGroup.room, on: openGroup.server)
},
encrypt: false,
onSuccess: handleSuccess,

View File

@ -39,8 +39,8 @@ extension ConfigurationMessage {
closedGroups.insert(closedGroup)
case .openGroup:
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
openGroups.insert("\(openGroup.server)/\(openGroup.room)?public_key=\(openGroup.publicKey)")
}
default: break

View File

@ -25,7 +25,7 @@ public extension Message {
}
if let thread = thread as? TSGroupThread, thread.isOpenGroup {
let openGroup: OpenGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)!
let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)!
return .openGroup(roomToken: openGroup.room, server: openGroup.server)
}

View File

@ -4,12 +4,13 @@ import Foundation
extension OpenGroupAPI {
public struct Capabilities: Codable {
public enum Capability: CaseIterable, Codable {
public enum Capability: Equatable, CaseIterable, Codable {
public static var allCases: [Capability] {
[.pysogs]
[.sogs, .blinding]
}
case pysogs
case sogs
case blinding // TODO: Get official name
/// Fallback case if the capability isn't supported by this version of the app
case unsupported(String)
@ -25,9 +26,7 @@ extension OpenGroupAPI {
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container: SingleValueDecodingContainer = try decoder.singleValueContainer()
let valueString: String = try container.decode(String.self)
public init(from valueString: String) {
let maybeValue: Capability? = Capability.allCases.first { $0.rawValue == valueString }
self = (maybeValue ?? .unsupported(valueString))
@ -38,3 +37,14 @@ extension OpenGroupAPI {
public let missing: [Capability]?
}
}
extension OpenGroupAPI.Capabilities.Capability {
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container: SingleValueDecodingContainer = try decoder.singleValueContainer()
let valueString: String = try container.decode(String.self)
self = OpenGroupAPI.Capabilities.Capability(from: valueString)
}
}

View File

@ -1,42 +1,66 @@
import Sodium
import SessionUtilitiesKit
// FIXME: We need to leave the @objc name as `SNOpenGroupV2` otherwise YapDatabase won't be able to decode it
@objc(SNOpenGroupV2)
public final class OpenGroupV2 : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public final class OpenGroup: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
@objc public let server: String
@objc public let room: String
public let id: String
@objc public let name: String
@objc public let publicKey: String
@objc public let name: String
@objc public let groupDescription: String? // API key is 'description'
/// The ID with which the image can be retrieved from the server.
public let imageID: String?
/// Monotonic room information counter that increases each time the room's metadata changes
public let infoUpdates: Int64
public init(server: String, room: String, name: String, publicKey: String, imageID: String?) {
public init(
server: String,
room: String,
publicKey: String,
name: String,
groupDescription: String?,
imageID: String?,
infoUpdates: Int64
) {
self.server = server.lowercased()
self.room = room
self.id = "\(server).\(room)"
self.name = name
self.publicKey = publicKey
self.name = name
self.groupDescription = groupDescription
self.imageID = imageID
self.infoUpdates = infoUpdates
}
// MARK: Coding
// MARK: - Coding
public init?(coder: NSCoder) {
server = coder.decodeObject(forKey: "server") as! String
room = coder.decodeObject(forKey: "room") as! String
self.id = "\(server).\(room)"
name = coder.decodeObject(forKey: "name") as! String
publicKey = coder.decodeObject(forKey: "publicKey") as! String
name = coder.decodeObject(forKey: "name") as! String
groupDescription = coder.decodeObject(forKey: "groupDescription") as? String
imageID = coder.decodeObject(forKey: "imageID") as! String?
infoUpdates = ((coder.decodeObject(forKey: "infoUpdates") as? Int64) ?? 0)
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(server, forKey: "server")
coder.encode(room, forKey: "room")
coder.encode(name, forKey: "name")
coder.encode(publicKey, forKey: "publicKey")
coder.encode(name, forKey: "name")
if let groupDescription = groupDescription { coder.encode(groupDescription, forKey: "groupDescription") }
if let imageID = imageID { coder.encode(imageID, forKey: "imageID") }
coder.encode(infoUpdates, forKey: "infoUpdates")
}
override public var description: String { "\(name) (Server: \(server), Room: \(room)" }

View File

@ -0,0 +1,46 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
extension OpenGroupAPI {
@objc(SOGSServer)
public final class Server: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
@objc public let name: String
public let capabilities: Capabilities
public init(
name: String,
capabilities: Capabilities
) {
self.name = name.lowercased()
self.capabilities = capabilities
}
// MARK: - Coding
public init?(coder: NSCoder) {
let capabilitiesString: [String] = coder.decodeObject(forKey: "capabilities") as! [String]
let missingCapabilitiesString: [String]? = coder.decodeObject(forKey: "missingCapabilities") as? [String]
name = coder.decodeObject(forKey: "name") as! String
capabilities = Capabilities(
capabilities: capabilitiesString.map { Capabilities.Capability(from: $0) },
missing: missingCapabilitiesString?.map { Capabilities.Capability(from: $0) }
)
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(capabilities.capabilities.map { $0.rawValue }, forKey: "capabilities")
coder.encode(capabilities.missing?.map { $0.rawValue }, forKey: "missingCapabilities")
}
override public var description: String {
"\(name) (Capabilities: [\(capabilities.capabilities.map { $0.rawValue }.joined(separator: ", "))], Missing: [\((capabilities.missing ?? []).map { $0.rawValue }.joined(separator: ", "))])"
}
}
}

View File

@ -62,8 +62,8 @@ public final class OpenGroupAPI: NSObject {
)
]
.appending(
dependencies.storage.getAllV2OpenGroups().values
.filter { $0.server == server.lowercased() } // Note: The `OpenGroupV2` type converts to lowercase in init
dependencies.storage.getAllOpenGroups().values
.filter { $0.server == server.lowercased() } // Note: The `OpenGroup` type converts to lowercase in init
.flatMap { openGroup -> [BatchRequestInfo] in
let lastSeqNo: Int64? = dependencies.storage.getLastMessageServerID(for: openGroup.room, on: server)
let targetSeqNo: Int64 = (lastSeqNo ?? 0)
@ -219,11 +219,9 @@ public final class OpenGroupAPI: NSObject {
on server: String,
whisperTo: String?,
whisperMods: Bool,
with serverPublicKey: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Message)> {
// TODO: Change this to use '.blinded' once it's working.
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, for: .standard, with: serverPublicKey) else {
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, to: roomToken, on: server, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
@ -265,11 +263,9 @@ public final class OpenGroupAPI: NSObject {
plaintext: Data,
in roomToken: String,
on server: String,
with serverPublicKey: String,
using dependencies: Dependencies = Dependencies()
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
// TODO: Change this to use '.blinded' once it's working.
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, for: .standard, with: serverPublicKey) else {
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, to: roomToken, on: server, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
@ -453,15 +449,14 @@ public final class OpenGroupAPI: NSObject {
.decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed)
}
public static func sendMessageRequest(_ plaintext: Data, to sessionId: String, on server: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
// TODO: Change this to use '.blinded' once it's working
guard let signedMessage: (data: Data, signature: Data) = sign(message: plaintext, for: .standard, with: serverPublicKey) else {
public static func sendMessageRequest(_ plaintext: Data, to blindedSessionId: String, on server: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> {
guard let signedMessage: Data = sign(message: plaintext, to: blindedSessionId, on: server, with: serverPublicKey, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
let requestBody: SendDirectMessageRequest = SendDirectMessageRequest(
data: signedMessage.data,
signature: signedMessage.signature
data: signedMessage
// signature: signedMessage.signature // TODO: Confirm whether this needs a signature??
)
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
@ -471,7 +466,7 @@ public final class OpenGroupAPI: NSObject {
let request: Request = Request(
method: .post,
server: server,
endpoint: .inboxFor(sessionId: sessionId),
endpoint: .inboxFor(sessionId: blindedSessionId),
body: body
)
@ -591,12 +586,31 @@ public final class OpenGroupAPI: NSObject {
// MARK: - Authentication
public static func sign(message: Data, for idType: IdPrefix, with publicKey: String, using dependencies: Dependencies = Dependencies()) -> (data: Data, signature: Data)? {
guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else {
return nil
/// Sign a message to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
public static func sign(message: Data, to roomToken: String, on serverName: String, using dependencies: Dependencies = Dependencies()) -> (data: Data, signature: Data)? {
let server: Server? = dependencies.storage.getOpenGroupServer(name: serverName)
let targetKeyPair: ECKeyPair
// Determine if we want to sign using standard or blinded keys based on the server capabilities (assume
// unblinded if we have none)
// TODO: Remove this (blinding will be required)
if server?.capabilities.capabilities.contains(.blinding) == true {
// TODO: Validate this 'openGroupId' is correct for the 'getOpenGroup' call
let openGroupId: String = "\(serverName).\(roomToken)"
// TODO: Validate this is the correct logic (Most likely not)
guard let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: openGroupId) else { return nil }
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroup.publicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
targetKeyPair = blindedKeyPair
}
guard let targetKeyPair: ECKeyPair = try? userKeyPair.convert(to: idType, with: publicKey) else {
return nil
else {
guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else { return nil }
targetKeyPair = userKeyPair
}
guard let signature = try? Ed25519.sign(message, with: targetKeyPair) else {
@ -607,7 +621,43 @@ public final class OpenGroupAPI: NSObject {
return (message, signature)
}
private static func sign(_ request: URLRequest, with publicKey: String, using dependencies: Dependencies = Dependencies()) -> URLRequest? {
/// Sign a blinded message request to be sent to a users inbox via SOGS v4
private static func sign(message: Data, to blindedSessionId: String, on serverName: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> Data? {
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
guard let blindedKeyPair: BlindedECKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
guard let blindedRecipientPublicKey: Data = String(blindedSessionId.suffix(from: blindedSessionId.index(blindedSessionId.startIndex, offsetBy: IdPrefix.blinded.rawValue.count))).dataFromHex() else {
return nil
}
/// Generate the sharedSecret by "a kB || kA || kB" where
/// a, A are the users private and public keys respectively,
/// kA is the users blinded public key
/// kB is the recipients blinded public key
let maybeSharedSecret: Data? = dependencies.sodium
.sharedEdSecret(userEdKeyPair.secretKey, blindedRecipientPublicKey.bytes)?
.appending(blindedKeyPair.publicKey.bytes)
.appending(blindedRecipientPublicKey.bytes)
guard let sharedSecret: Data = maybeSharedSecret else { return nil }
guard let intermediateHash: Bytes = dependencies.genericHash.hash(message: sharedSecret.bytes) else { return nil }
/// Generate the inner message by "message || A" where
/// A is the sender's ed25519 master pubkey (**not** kA blinded pubkey)
let innerMessage: Bytes = (message.bytes + userEdKeyPair.publicKey)
guard let (ciphertext, nonce) = dependencies.aeadXChaCha20Poly1305Ietf.encrypt(message: innerMessage, secretKey: intermediateHash) else {
return nil
}
/// Generate the final data by "b'\x00' + ciphertext + nonce"
let finalData: Bytes = [0] + ciphertext + nonce
return Data(finalData)
}
/// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
private static func sign(_ request: URLRequest, for serverName: String, with serverPublicKey: String, using dependencies: Dependencies = Dependencies()) -> URLRequest? {
guard let url: URL = request.url else { return nil }
var updatedRequest: URLRequest = request
@ -616,55 +666,78 @@ public final class OpenGroupAPI: NSObject {
let method: String = (request.httpMethod ?? "GET")
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
let nonce: Data = Data(dependencies.nonceGenerator.nonce())
let server: Server? = dependencies.storage.getOpenGroupServer(name: serverName)
let userPublicKeyHex: String
let signatureBytes: Bytes
guard let publicKeyData: Data = publicKey.dataFromHex() else { return nil }
guard let userKeyPair: ECKeyPair = dependencies.storage.getUserKeyPair() else {
return nil
}
// guard let blindedKeyPair: ECKeyPair = try? userKeyPair.convert(to: .blinded, with: publicKey) else {
// return nil
// }
// TODO: Change this back once you figure out why it's busted.
let blindedKeyPair: ECKeyPair = userKeyPair
guard let serverPublicKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
guard let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes else { return nil }
guard let userEdKeyPair: Box.KeyPair = dependencies.storage.getUserED25519KeyPair() else { return nil }
/// Generate the sharedSecret by "aB || A || B" where
/// a, A are the users private and public keys respectively,
/// B is the SOGS public key
let maybeSharedSecret: Data? = dependencies.sodium.sharedSecret(blindedKeyPair.privateKey.bytes, publicKeyData.bytes)?
.appending(blindedKeyPair.publicKey)
.appending(publicKeyData.bytes)
/// Get a hash of any body content
let bodyHash: Bytes? = {
// Note: We need the `!body.isEmpty` check because of the default `Data()` value when trying to
// init data from the httpBodyStream
guard let body: Data = (request.httpBody ?? request.httpBodyStream.map { ((try? Data(from: $0)) ?? Data()) }), !body.isEmpty else {
return nil
}
return dependencies.genericHash.hash(message: body.bytes, outputLength: 64)
}()
/// Generate the hash to be sent along with the request
/// intermediateHash = Blake2B(sharedSecret, size=42, salt=noncebytes, person='sogs.shared_keys')
/// secretHash = Blake2B(
/// Method || Path || Timestamp || Body,
/// size=42,
/// key=r,
/// salt=noncebytes,
/// person='sogs.auth_header'
/// )
let secretHashMessage: Bytes = method.bytes
/// Generate the signature message
/// "ServerPubkey || Nonce || Timestamp || Method || Path || Blake2b Hash(Body)
/// `ServerPubkey`
/// `Nonce`
/// `Timestamp` is the bytes of an ascii decimal string
/// `Method`
/// `Path`
/// `Body` is a Blake2b hash of the data (if there is a body)
let signatureMessageBytes: Bytes = serverPublicKeyData.bytes
.appending(nonce.bytes)
.appending(timestampBytes)
.appending(method.bytes)
.appending(path.bytes)
.appending("\(timestamp)".bytes)
.appending(request.httpBody?.bytes ?? []) // TODO: Might need to do the 'httpBodyStream' as well???.
print("RAWR 1 \(blindedKeyPair.hexEncodedPublicKey)")
print("RAWR 2 \(maybeSharedSecret?.hexadecimalString)")
print("RAWR '\(String(describing: String(data: Data(secretHashMessage), encoding: .utf8)))'")
guard let sharedSecret: Data = maybeSharedSecret else { return nil }
guard let intermediateHash: Bytes = dependencies.genericHash.hashSaltPersonal(message: sharedSecret.bytes, outputLength: 42, key: nil, salt: nonce.bytes, personal: Personalization.sharedKeys.bytes) else {
return nil
.appending(bodyHash ?? [])
// Determine if we want to sign using standard or blinded keys based on the server capabilities (assume
// unblinded if we have none)
// TODO: Remove this (blinding will be required)
if server?.capabilities.capabilities.contains(.blinding) == true {
// TODO: More testing of this blinded id signing (though it seems to be working!!!)
guard let blindedKeyPair: Box.KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
userPublicKeyHex = IdPrefix.blinded.hexEncodedPublicKey(for: blindedKeyPair.publicKey)
guard let signatureResult: Bytes = Sodium().sogsSignature(message: signatureMessageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey) else {
return nil
}
signatureBytes = signatureResult
}
guard let secretHash: Bytes = dependencies.genericHash.hashSaltPersonal(message: secretHashMessage, outputLength: 42, key: intermediateHash, salt: nonce.bytes, personal: Personalization.authHeader.bytes) else {
return nil
else {
userPublicKeyHex = IdPrefix.unblinded.hexEncodedPublicKey(for: userEdKeyPair.publicKey)
// TODO: shift this to dependencies
guard let signatureResult: Bytes = Sodium().sign.signature(message: signatureMessageBytes, secretKey: userEdKeyPair.secretKey) else {
return nil
}
signatureBytes = signatureResult
}
print("RAWR3 '\(intermediateHash.toHexString())'") // This is the one we can compare
print("RAWR4 '\(secretHash.toHexString())'")
print("RAWR X-SOGS-Pubkey: \(userPublicKeyHex)")
print("RAWR X-SOGS-Timestamp: \(timestamp)")
print("RAWR X-SOGS-Nonce: \(nonce.base64EncodedString())")
print("RAWR X-SOGS-Signature: \(signatureBytes.toBase64())")
updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:])
.updated(with: [
Header.sogsPubKey.rawValue: blindedKeyPair.hexEncodedPublicKey,
Header.sogsPubKey.rawValue: userPublicKeyHex,
Header.sogsTimestamp.rawValue: "\(timestamp)",
Header.sogsNonce.rawValue: nonce.base64EncodedString(),
Header.sogsHash.rawValue: secretHash.toBase64()
Header.sogsSignature.rawValue: signatureBytes.toBase64()
])
return updatedRequest
@ -683,13 +756,13 @@ public final class OpenGroupAPI: NSObject {
urlRequest.httpBody = request.body
if request.useOnionRouting {
guard let publicKey = dependencies.storage.getOpenGroupPublicKey(for: request.server) else {
guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else {
return Promise(error: Error.noPublicKey)
}
if request.isAuthRequired {
// Attempt to sign the request with the new auth
guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey, using: dependencies) else {
guard let signedRequest: URLRequest = sign(urlRequest, for: request.server, with: publicKey, using: dependencies) else {
return Promise(error: Error.signingFailed)
}
@ -826,7 +899,7 @@ public final class OpenGroupAPI: NSObject {
@available(*, deprecated, message: "Use poll or batch instead")
public static func legacyCompactPoll(_ server: String) -> Promise<LegacyCompactPollResponse> {
let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage
let rooms: [String] = storage.getAllV2OpenGroups().values
let rooms: [String] = storage.getAllOpenGroups().values
.filter { $0.server == server }
.map { $0.room }
var getAuthTokenPromises: [String: Promise<String>] = [:]
@ -1037,7 +1110,7 @@ public final class OpenGroupAPI: NSObject {
let storage = SNMessagingKitConfiguration.shared.storage
storage.write { transaction in
storage.setUserCount(to: response.memberCount, forV2OpenGroupWithID: "\(server).\(room)", using: transaction)
storage.setUserCount(to: response.memberCount, forOpenGroupWithID: "\(server).\(room)", using: transaction)
}
return response.memberCount

View File

@ -19,7 +19,7 @@ public final class OpenGroupManager: NSObject {
guard !isPolling else { return }
isPolling = true
pollers = Set(Storage.shared.getAllV2OpenGroups().values.map { $0.server })
pollers = Set(Storage.shared.getAllOpenGroups().values.map { $0.server })
.reduce(into: [:]) { prev, server in
pollers[server]?.stop() // Should never occur
@ -58,8 +58,7 @@ public final class OpenGroupManager: NSObject {
room,
publicKey: publicKey,
for: roomToken,
on: server,
isBackgroundPoll: false
on: server
) {
seal.fulfill(())
}
@ -72,11 +71,11 @@ public final class OpenGroupManager: NSObject {
return promise
}
public func delete(_ openGroup: OpenGroupV2, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
let storage = SNMessagingKitConfiguration.shared.storage
// Stop the poller if needed
let openGroups = storage.getAllV2OpenGroups().values.filter { $0.server == openGroup.server }
let openGroups = storage.getAllOpenGroups().values.filter { $0.server == openGroup.server }
if openGroups.count == 1 && openGroups.last == openGroup {
let poller = pollers[openGroup.server]
poller?.stop()
@ -97,7 +96,7 @@ public final class OpenGroupManager: NSObject {
let _ = OpenGroupAPI.legacyDeleteAuthToken(for: openGroup.room, on: openGroup.server)
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
Storage.shared.removeV2OpenGroup(for: thread.uniqueId!, using: transaction)
Storage.shared.removeOpenGroup(for: thread.uniqueId!, using: transaction)
// Only remove the open group public key if the user isn't in any other rooms
if openGroups.count <= 1 {
@ -107,6 +106,21 @@ public final class OpenGroupManager: NSObject {
// MARK: - Response Processing
internal static func handleCapabilities(
_ capabilities: OpenGroupAPI.Capabilities,
on server: String,
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()
) {
dependencies.storage.write { transaction in
let updatedServer: OpenGroupAPI.Server = OpenGroupAPI.Server(
name: server,
capabilities: capabilities
)
dependencies.storage.storeOpenGroupServer(updatedServer, using: transaction)
}
}
internal static func handleMessages(
_ messages: [OpenGroupAPI.Message],
for roomToken: String,
@ -154,7 +168,7 @@ public final class OpenGroupManager: NSObject {
// Handle any deletions that are needed
guard !messageServerIDsToRemove.isEmpty else { return }
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else { return }
guard let threadID = dependencies.storage.v2GetThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
guard let threadID = dependencies.storage.getThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else {
return
}
@ -174,7 +188,6 @@ public final class OpenGroupManager: NSObject {
publicKey: String,
for roomToken: String,
on server: String,
isBackgroundPoll: Bool,
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies(),
completion: (() -> ())? = nil
) {
@ -183,7 +196,6 @@ public final class OpenGroupManager: NSObject {
publicKey: publicKey,
for: roomToken,
on: server,
isBackgroundPoll: isBackgroundPoll,
using: dependencies,
completion: completion
)
@ -194,7 +206,6 @@ public final class OpenGroupManager: NSObject {
publicKey maybePublicKey: String?,
for roomToken: String,
on server: String,
isBackgroundPoll: Bool,
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies(),
completion: (() -> ())? = nil
) {
@ -215,8 +226,8 @@ public final class OpenGroupManager: NSObject {
with: { transaction in
let transaction = transaction as! YapDatabaseReadWriteTransaction
let thread = TSGroupThread.getOrCreateThread(with: initialModel, transaction: transaction)
let existingOpenGroup: OpenGroupV2? = thread.uniqueId.flatMap { uniqueId -> OpenGroupV2? in
dependencies.storage.getV2OpenGroup(for: uniqueId)
let existingOpenGroup: OpenGroup? = thread.uniqueId.flatMap { uniqueId -> OpenGroup? in
dependencies.storage.getOpenGroup(for: uniqueId)
}
guard let threadUniqueId: String = thread.uniqueId else { return }
@ -230,7 +241,7 @@ public final class OpenGroupManager: NSObject {
groupType: .openGroup,
adminIds: (pollInfo.admins ?? thread.groupModel.groupAdminIds)
)
let updatedOpenGroup: OpenGroupV2 = OpenGroupV2(
let updatedOpenGroup: OpenGroup = OpenGroup(
server: server,
room: (pollInfo.token ?? roomToken),
publicKey: publicKey,
@ -239,7 +250,7 @@ public final class OpenGroupManager: NSObject {
imageID: (pollInfo.imageId.map { "\($0)" } ?? existingOpenGroup?.imageID),
infoUpdates: ((pollInfo.infoUpdates ?? existingOpenGroup?.infoUpdates) ?? 0)
)
let existingUserCount: UInt64? = dependencies.storage.getUserCount(forV2OpenGroupWithID: updatedOpenGroup.id)
let existingUserCount: UInt64? = dependencies.storage.getUserCount(forOpenGroupWithID: updatedOpenGroup.id)
// - Thread changes
thread.shouldBeVisible = true
@ -247,12 +258,12 @@ public final class OpenGroupManager: NSObject {
thread.save(with: transaction)
// - Open Group changes
dependencies.storage.setV2OpenGroup(updatedOpenGroup, for: threadUniqueId, using: transaction)
dependencies.storage.setOpenGroup(updatedOpenGroup, for: threadUniqueId, using: transaction)
// - User Count
dependencies.storage.setUserCount(
to: ((pollInfo.activeUsers.map { UInt64($0) } ?? existingUserCount) ?? 0),
forV2OpenGroupWithID: updatedOpenGroup.id,
forOpenGroupWithID: updatedOpenGroup.id,
using: transaction
)
},

View File

@ -9,6 +9,7 @@ extension OpenGroupAPI {
let api: OnionRequestAPIType.Type
let storage: SessionMessagingKitStorageProtocol
let sodium: SodiumType
let aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType
let genericHash: GenericHashType
let nonceGenerator: NonceGenerator16ByteType
let date: Date
@ -17,6 +18,7 @@ extension OpenGroupAPI {
api: OnionRequestAPIType.Type = OnionRequestAPI.self,
storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage,
sodium: SodiumType = Sodium(),
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
genericHash: GenericHashType? = nil,
nonceGenerator: NonceGenerator16ByteType = NonceGenerator16Byte(),
date: Date = Date()
@ -24,6 +26,7 @@ extension OpenGroupAPI {
self.api = api
self.storage = storage
self.sodium = sodium
self.aeadXChaCha20Poly1305Ietf = (aeadXChaCha20Poly1305Ietf ?? sodium.getAeadXChaCha20Poly1305Ietf())
self.genericHash = (genericHash ?? sodium.getGenericHash())
self.nonceGenerator = nonceGenerator
self.date = date
@ -35,6 +38,7 @@ extension OpenGroupAPI {
api: OnionRequestAPIType.Type? = nil,
storage: SessionMessagingKitStorageProtocol? = nil,
sodium: SodiumType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
genericHash: GenericHashType? = nil,
nonceGenerator: NonceGenerator16ByteType? = nil,
date: Date? = nil
@ -43,6 +47,7 @@ extension OpenGroupAPI {
api: (api ?? self.api),
storage: (storage ?? self.storage),
sodium: (sodium ?? self.sodium),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self.aeadXChaCha20Poly1305Ietf),
genericHash: (genericHash ?? self.genericHash),
nonceGenerator: (nonceGenerator ?? self.nonceGenerator),
date: (date ?? self.date)

View File

@ -5,23 +5,49 @@ import Sodium
public protocol SodiumType {
func getGenericHash() -> GenericHashType
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair?
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret?
func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret?
}
public protocol AeadXChaCha20Poly1305IetfType {
func encrypt(message: Bytes, secretKey: Aead.XChaCha20Poly1305Ietf.Key, additionalData: Bytes?) -> (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce)?
}
public protocol GenericHashType {
func hash(message: Bytes, key: Bytes?) -> Bytes?
func hash(message: Bytes, outputLength: Int) -> Bytes?
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes?
}
// MARK: - Default Values
extension AeadXChaCha20Poly1305IetfType {
func encrypt(message: Bytes, secretKey: Aead.XChaCha20Poly1305Ietf.Key) -> (authenticatedCipherText: Bytes, nonce: Aead.XChaCha20Poly1305Ietf.Nonce)? {
return encrypt(message: message, secretKey: secretKey, additionalData: nil)
}
}
extension GenericHashType {
func hash(message: Bytes) -> Bytes? { return hash(message: message, key: nil) }
func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? {
return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal)
}
}
// MARK: - Conformance
extension Sodium: SodiumType {
public func getGenericHash() -> GenericHashType { return genericHash }
public func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return aead.xchacha20poly1305ietf }
public func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair) -> Box.KeyPair? {
return blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: edKeyPair, genericHash: getGenericHash())
}
}
extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {}
extension GenericHash: GenericHashType {}

View File

@ -30,10 +30,10 @@ public final class MentionsManager : NSObject {
guard let cache = userPublicKeyCache[threadID] else { return [] }
var candidates: [Mention] = []
// Gather candidates
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
let openGroup = Storage.shared.getOpenGroup(for: threadID)
storage.dbReadConnection.read { transaction in
candidates = cache.compactMap { publicKey in
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
let context: Contact.Context = (openGroup != nil) ? .openGroup : .regular
let displayNameOrNil = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
guard let displayName = displayNameOrNil else { return nil }
guard !displayName.hasPrefix("Anonymous") else { return nil }

View File

@ -287,11 +287,11 @@ public final class MessageSender : NSObject {
message.sentTimestamp = NSDate.millisecondTimestamp()
}
guard let threadId: String = message.threadID, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else {
guard let threadId: String = message.threadID, let openGroup = Storage.shared.getOpenGroup(for: threadId) else {
preconditionFailure()
}
if let userDerivedKey: ECKeyPair = try? OWSIdentityManager.shared().identityKeyPair()?.convert(to: .blinded, with: openGroupV2.publicKey) {
// TODO: Check if blinding is enabled on this server?
if let userDerivedKey: ECKeyPair = try? OWSIdentityManager.shared().identityKeyPair()?.convert(to: .blinded, with: openGroup.publicKey) {
message.sender = userDerivedKey.hexEncodedPublicKey
}
@ -368,19 +368,13 @@ public final class MessageSender : NSObject {
preconditionFailure()
}
// TODO: Determine if the 'getV2OpenGroup' call will cause issues.
guard let threadId: String = message.threadID, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else {
preconditionFailure()
}
OpenGroupAPI
.send(
plaintext,
to: room,
on: server,
whisperTo: whisperTo,
whisperMods: whisperMods,
with: openGroupV2.publicKey
whisperMods: whisperMods
)
.done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in
message.openGroupServerMessageID = given(data.seqNo) { UInt64($0) }

View File

@ -53,9 +53,8 @@ public final class PushNotificationAPI : NSObject {
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
// TODO: Update this to use the V4 union requests once supported
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
.map2 { response in
guard let response: UnregisterResponse = try? response.decoded(as: UnregisterResponse.self) else {
.map2 { _, response in
guard let response: UnregisterResponse = try? response?.decoded(as: UnregisterResponse.self) else {
return SNLog("Couldn't unregister from push notifications.")
}
guard response.code != 0 else {
@ -104,9 +103,8 @@ public final class PushNotificationAPI : NSObject {
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
// TODO: Update this to use the V4 union requests once supported
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
.map2 { response in
guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else {
.map2 { _, response in
guard let response: RegisterResponse = try? response?.decoded(as: RegisterResponse.self) else {
return SNLog("Couldn't register device token.")
}
guard response.code != 0 else {
@ -152,9 +150,8 @@ public final class PushNotificationAPI : NSObject {
let promise: Promise<Void> = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) {
// TODO: Update this to use the V4 union requests once supported
OnionRequestAPI.sendOnionRequest(request, to: server, using: .v2, with: serverPublicKey)
.map2 { json in try JSONSerialization.data(withJSONObject: json, options: []) }
.map2 { response in
guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else {
.map2 { _, response in
guard let response: RegisterResponse = try? response?.decoded(as: RegisterResponse.self) else {
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
}
guard response.code != 0 else {

View File

@ -82,31 +82,41 @@ extension OpenGroupAPI {
private func handlePollResponse(_ response: [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable)], isBackgroundPoll: Bool) {
response.forEach { endpoint, response in
switch endpoint {
case .capabilities:
guard let responseData: BatchSubResponse<Capabilities> = response.data as? BatchSubResponse<Capabilities> else {
SNLog("Open group polling failed due to invalid data.")
return
}
OpenGroupManager.handleCapabilities(
responseData.body,
on: server
)
case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _):
guard let responseData: [OpenGroupAPI.Message] = response.data as? [OpenGroupAPI.Message] else {
guard let responseData: BatchSubResponse<[Message]> = response.data as? BatchSubResponse<[Message]> else {
SNLog("Open group polling failed due to invalid data.")
return
}
OpenGroupManager.handleMessages(
responseData,
responseData.body,
for: roomToken,
on: server,
isBackgroundPoll: isBackgroundPoll
)
case .roomPollInfo(let roomToken, _):
guard let responseData: OpenGroupAPI.RoomPollInfo = response.data as? OpenGroupAPI.RoomPollInfo else {
guard let responseData: BatchSubResponse<RoomPollInfo> = response.data as? BatchSubResponse<RoomPollInfo> else {
SNLog("Open group polling failed due to invalid data.")
return
}
OpenGroupManager.handlePollInfo(
responseData,
responseData.body,
publicKey: nil,
for: roomToken,
on: server,
isBackgroundPoll: isBackgroundPoll
on: server
)
default: break // No custom handling needed
@ -154,7 +164,7 @@ extension OpenGroupAPI {
let deletedMessageServerIDs = Set((body.deletions ?? []).map { UInt64($0.deletedMessageID) })
storage.write { transaction in
let transaction = transaction as! YapDatabaseReadWriteTransaction
guard let threadID = storage.v2GetThreadID(for: openGroupID),
guard let threadID = storage.getThreadID(for: openGroupID),
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
var messagesToRemove: [TSMessage] = []

View File

@ -1,7 +1,7 @@
import PromiseKit
import Sodium
public protocol SessionMessagingKitStorageProtocol: SessionMessagingKitOpenGroupStorageProtocol {
public protocol SessionMessagingKitStorageProtocol {
// MARK: - Shared
@ -45,11 +45,22 @@ public protocol SessionMessagingKitStorageProtocol: SessionMessagingKitOpenGroup
// MARK: - Open Groups
func getAllV2OpenGroups() -> [String:OpenGroupV2]
func getV2OpenGroup(for threadID: String) -> OpenGroupV2?
func v2GetThreadID(for v2OpenGroupID: String) -> String?
func getAllOpenGroups() -> [String: OpenGroup]
func getThreadID(for openGroupID: String) -> String?
func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any)
func getOpenGroupImage(for room: String, on server: String) -> Data?
func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any)
func getOpenGroup(for threadID: String) -> OpenGroup?
func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any)
func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64?
func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any)
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server?
func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any)
// MARK: - Open Group Public Keys
func getOpenGroupPublicKey(for server: String) -> String?

View File

@ -17,7 +17,7 @@ extension Sign {
&x25519PublicKey,
ed25519PublicKey
)
return x25519PublicKey
}
@ -43,15 +43,136 @@ extension Sign {
extension Sodium {
public typealias SharedSecret = Data
private static let publicKeyBytes: Int = Int(crypto_scalarmult_bytes())
private static let sharedSecretBytes: Int = Int(crypto_scalarmult_bytes())
private static let scalarLength: Int = Int(crypto_core_ed25519_scalarbytes()) // 32
private static let noClampLength: Int = Int(crypto_scalarmult_ed25519_bytes()) // 32
private static let scalarMultLength: Int = Int(crypto_scalarmult_bytes()) // 32
private static let publicKeyLength: Int = Int(crypto_scalarmult_bytes()) // 32
private static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64
public func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> SharedSecret? {
guard firstKeyBytes.count == Sodium.publicKeyBytes && secondKeyBytes.count == Sodium.publicKeyBytes else {
public func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else {
return nil
}
let sharedSecretPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.sharedSecretBytes)
/// 64-byte blake2b hash then reduce to get the blinding factor:
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
guard let serverPubKeyData: Data = serverPublicKey.dataFromHex() else { return nil }
guard let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
return nil
}
/// Reduce the server public key into an ed25519 scalar (`k`)
let kPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let kResult = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let serverPublicKeyHashBaseAddress: UnsafePointer<UInt8> = serverPublicKeyHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
crypto_core_ed25519_scalar_reduce(kPtr, serverPublicKeyHashBaseAddress)
return 0
}
/// Ensure the above worked
guard kResult == 0 else { return nil }
/// Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
/// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
/// same secret scalar secret. (And so this is just the most convenient way to get 'a' out of
/// a sodium Ed25519 secret key).
/// a = s.to_curve25519_private_key().encode()
let secretKeyBytes: Bytes = [UInt8](edKeyPair.secretKey)
let aPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarMultLength)
let aResult = secretKeyBytes.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let secretKeyBaseAddress: UnsafePointer<UInt8> = secretKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
return crypto_sign_ed25519_sk_to_curve25519(aPtr, secretKeyBaseAddress)
}
/// Ensure the above worked
guard aResult == 0 else { return nil }
/// Generate the blinded key pair `ka`, `kA`
let kaPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.secretKeyLength)
let kAPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.publicKeyLength)
crypto_core_ed25519_scalar_mul(kaPtr, kPtr, aPtr)
guard crypto_scalarmult_ed25519_base_noclamp(kAPtr, kaPtr) == 0 else { return nil }
return Box.KeyPair(
publicKey: Data(bytes: kAPtr, count: Sodium.publicKeyLength).bytes,
secretKey: Data(bytes: kaPtr, count: Sodium.secretKeyLength).bytes
)
}
/// Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
/// construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
/// pubkeys (This doesn't affect verification at all).
public func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
/// H_rh = sha512(s.encode()).digest()[32:]
let H_rh: Bytes = Bytes(secretKey.sha512().suffix(32))
/// r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
let combinedHashBytes: Bytes = (H_rh + kA + message).sha512()
let rPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let rResult = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let combinedHashBaseAddress: UnsafePointer<UInt8> = combinedHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
crypto_core_ed25519_scalar_reduce(rPtr, combinedHashBaseAddress)
return 0
}
/// Ensure the above worked
guard rResult == 0 else { return nil }
/// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
let sig_RPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
guard crypto_scalarmult_ed25519_base_noclamp(sig_RPtr, rPtr) == 0 else { return nil }
/// HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
let sig_RBytes: Bytes = Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes
let HRAMHashBytes: Bytes = (sig_RBytes + kA + message).sha512()
let HRAMPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let HRAMResult = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let HRAMHashBaseAddress: UnsafePointer<UInt8> = HRAMHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
crypto_core_ed25519_scalar_reduce(HRAMPtr, HRAMHashBaseAddress)
return 0
}
/// Ensure the above worked
guard HRAMResult == 0 else { return nil }
/// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
let sig_sMulPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let sig_sPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let sig_sResult = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kaBaseAddress: UnsafePointer<UInt8> = kaPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
crypto_core_ed25519_scalar_mul(sig_sMulPtr, HRAMPtr, kaBaseAddress)
crypto_core_ed25519_scalar_add(sig_sPtr, rPtr, sig_sMulPtr)
return 0
}
guard sig_sResult == 0 else { return nil }
/// full_sig = sig_R + sig_s
return (Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes + Data(bytes: sig_sPtr, count: Sodium.scalarLength).bytes)
}
public func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> SharedSecret? {
let sharedSecretPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
let result = secondKeyBytes.withUnsafeBytes { (secondKeyPtr: UnsafeRawBufferPointer) -> Int32 in
return firstKeyBytes.withUnsafeBytes { (firstKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let firstKeyBaseAddress: UnsafePointer<UInt8> = firstKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
@ -61,13 +182,39 @@ extension Sodium {
return -1
}
return crypto_scalarmult_ed25519_noclamp(sharedSecretPtr, firstKeyBaseAddress, secondKeyBaseAddress)
}
}
guard result == 0 else { return nil }
return Data(bytes: sharedSecretPtr, count: Sodium.scalarMultLength)
}
public func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> SharedSecret? {
guard firstKeyBytes.count == Sodium.publicKeyLength && secondKeyBytes.count == Sodium.publicKeyLength else {
return nil
}
let sharedSecretPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarMultLength)
let result = secondKeyBytes.withUnsafeBytes { (secondKeyPtr: UnsafeRawBufferPointer) -> Int32 in
return firstKeyBytes.withUnsafeBytes { (firstKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let firstKeyBaseAddress: UnsafePointer<UInt8> = firstKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
guard let secondKeyBaseAddress: UnsafePointer<UInt8> = secondKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1
}
//crypto_sign_ed25519_publickeybytes
//crypto_scalarmult_curve25519(<#T##q: UnsafeMutablePointer<UInt8>##UnsafeMutablePointer<UInt8>#>, <#T##n: UnsafePointer<UInt8>##UnsafePointer<UInt8>#>, <#T##p: UnsafePointer<UInt8>##UnsafePointer<UInt8>#>)
return crypto_scalarmult(sharedSecretPtr, firstKeyBaseAddress, secondKeyBaseAddress)
}
}
guard result == 0 else { return nil }
return Data(bytes: sharedSecretPtr, count: Sodium.sharedSecretBytes)
return Data(bytes: sharedSecretPtr, count: Sodium.scalarMultLength)
}
}

View File

@ -80,8 +80,8 @@ class OpenGroupAPITests: XCTestCase {
date: Date(timeIntervalSince1970: 1234567890)
)
testStorage.mockData[.allV2OpenGroups] = [
"0": OpenGroupV2(
testStorage.mockData[.allOpenGroups] = [
"0": OpenGroup(
server: "testServer",
room: "testRoom",
publicKey: "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d",
@ -446,9 +446,9 @@ class OpenGroupAPITests: XCTestCase {
expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("057aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d"))
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[Header.sogsHash.rawValue]).to(equal("fxqLy5ZDWCsLQpwLw0Dax+4xe7cG2vPRk1NlHORIm0DPd3o9UA24KLZY"))
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("fxqLy5ZDWCsLQpwLw0Dax+4xe7cG2vPRk1NlHORIm0DPd3o9UA24KLZY"))
}
func testItFailsToSignIfTheServerPublicKeyIsInvalid() throws {
@ -495,6 +495,11 @@ class OpenGroupAPITests: XCTestCase {
class InvalidSodium: SodiumType {
func getGenericHash() -> GenericHashType { return Sodium().genericHash }
func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret? { return nil }
func sharedEdSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> Sodium.SharedSecret? { return nil }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return Sodium().aead.xchacha20poly1305ietf }
func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? {
return nil
}
}
dependencies = dependencies.with(sodium: InvalidSodium())
@ -518,6 +523,7 @@ class OpenGroupAPITests: XCTestCase {
func testItFailsToSignIfTheIntermediateHashDoesNotGetGenerated() throws {
class InvalidGenericHash: GenericHashType {
func hash(message: Bytes, key: Bytes?) -> Bytes? { return nil }
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? {
return nil
}
@ -546,6 +552,7 @@ class OpenGroupAPITests: XCTestCase {
class InvalidSecondGenericHash: GenericHashType {
static var didSucceedOnce: Bool = false
func hash(message: Bytes, key: Bytes?) -> Bytes? { return nil }
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? {
if !InvalidSecondGenericHash.didSucceedOnce {
InvalidSecondGenericHash.didSucceedOnce = true

View File

@ -10,10 +10,11 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
// MARK: - Mockable
enum DataKey: Hashable {
case allV2OpenGroups
case allOpenGroups
case openGroupPublicKeys
case userKeyPair
case openGroup
case openGroupServer
case openGroupImage
case openGroupUserCount
}
@ -72,11 +73,28 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
// MARK: - Open Groups
func getAllV2OpenGroups() -> [String: OpenGroupV2] { return (mockData[.allV2OpenGroups] as! [String: OpenGroupV2]) }
func getV2OpenGroup(for threadID: String) -> OpenGroupV2? { return (mockData[.openGroup] as? OpenGroupV2) }
func v2GetThreadID(for v2OpenGroupID: String) -> String? { return nil }
func getAllOpenGroups() -> [String: OpenGroup] { return (mockData[.allOpenGroups] as! [String: OpenGroup]) }
func getThreadID(for v2OpenGroupID: String) -> String? { return nil }
func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any) {}
func getOpenGroupImage(for room: String, on server: String) -> Data? { return (mockData[.openGroupImage] as? Data) }
func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) {
mockData[.openGroupImage] = data
}
func getOpenGroup(for threadID: String) -> OpenGroup? { return (mockData[.openGroup] as? OpenGroup) }
func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) { mockData[.openGroup] = openGroup }
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? { return mockData[.openGroupServer] as? OpenGroupAPI.Server }
func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server }
func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? {
return (mockData[.openGroupUserCount] as? UInt64)
}
func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any) {
mockData[.openGroupUserCount] = newValue
}
// MARK: - Open Group Public Keys
func getOpenGroupPublicKey(for server: String) -> String? {
@ -111,24 +129,3 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsIncomingMessageID: String, using transaction: Any) {}
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any) {}
}
// MARK: - SessionMessagingKitOpenGroupStorageProtocol
extension TestStorage: SessionMessagingKitOpenGroupStorageProtocol {
func getOpenGroupImage(for room: String, on server: String) -> Data? { return (mockData[.openGroupImage] as? Data) }
func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) {
mockData[.openGroupImage] = data
}
func setV2OpenGroup(_ openGroup: OpenGroupV2, for threadID: String, using transaction: Any) {
mockData[.openGroup] = openGroup
}
func getUserCount(forV2OpenGroupWithID openGroupID: String) -> UInt64? {
return (mockData[.openGroupUserCount] as? UInt64)
}
func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any) {
mockData[.openGroupUserCount] = newValue
}
}

View File

@ -1,6 +1,6 @@
import Foundation
internal extension Data {
public extension Data {
init(from inputStream: InputStream) throws {
self.init()

View File

@ -20,9 +20,3 @@ public extension ECKeyPair {
return true
}
}
public extension BlindedECKeyPair {
@objc override var hexEncodedPublicKey: String {
return IdPrefix.blinded.rawValue + publicKey.map { String(format: "%02hhx", $0) }.joined()
}
}

View File

@ -1,20 +1,24 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Curve25519Kit
/// The `BlindedECKeyPair` is essentially the same as the `ECKeyPair` except it allows us to more easily distinguish between the two,
/// additionally when generating the `hexEncodedPublicKey` value it will apply the correct prefix
public class BlindedECKeyPair: ECKeyPair {}
public enum IdPrefix: String, CaseIterable {
case standard = "05" // Used for identified users, open groups, etc.
case blinded = "15" // Used for participants in open groups
case blinded = "15" // Used for participants in open groups with blinding enabled
case unblinded = "00" // Used for participants in open groups with blinding disabled
public init?(with sessionId: String) {
// TODO: Determine if we want this 'idPrefix' method (would need to validate both `ECKeyPair` and `Box.KeyPair` types)
guard ECKeyPair.isValidHexEncodedPublicKey(candidate: sessionId) else { return nil }
guard let targetPrefix: IdPrefix = IdPrefix(rawValue: String(sessionId.prefix(2))) else { return nil }
self = targetPrefix
}
public func hexEncodedPublicKey(for publicKey: Bytes) -> String {
return self.rawValue + publicKey.map { String(format: "%02hhx", $0) }.joined()
}
}

View File

@ -43,16 +43,16 @@ extension MessageSender {
let attachmentsToUpload = attachments.filter { !$0.isUploaded }
let attachmentUploadPromises: [Promise<Void>] = attachmentsToUpload.map { stream in
let storage = SNMessagingKitConfiguration.shared.storage
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
let (promise, seal) = Promise<Void>.pending()
AttachmentUploadJob.upload(
stream,
using: { data in
// TODO: Update to non-legacy version
// TODO: Update to non-legacy version.
OpenGroupAPI.legacyUpload(
data,
to: v2OpenGroup.room,
on: v2OpenGroup.server
to: openGroup.room,
on: openGroup.server
)
},
encrypt: false,
@ -90,7 +90,7 @@ extension MessageSender {
let attachmentsToUpload = attachments.filter { !$0.isUploaded }
let attachmentUploadPromises: [Promise<Void>] = attachmentsToUpload.map { stream in
let storage = SNMessagingKitConfiguration.shared.storage
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
let (promise, seal) = Promise<Void>.pending()
AttachmentUploadJob.upload(
stream,
@ -98,8 +98,8 @@ extension MessageSender {
// TODO: Update to non-legacy version
OpenGroupAPI.legacyUpload(
data,
to: v2OpenGroup.room,
on: v2OpenGroup.server
to: openGroup.room,
on: openGroup.server
)
},
encrypt: false,