Re-organize files

This commit is contained in:
nielsandriesse 2020-11-09 10:58:47 +11:00
parent 41d078d012
commit a3382f41d4
70 changed files with 86 additions and 1961 deletions

View File

@ -121,7 +121,7 @@ target 'SessionSnodeKit' do
pod 'PromiseKit', :inhibit_warnings => true
end
target 'SessionUtilities' do
target 'SessionUtilitiesKit' do
pod 'CryptoSwift', :inhibit_warnings => true
pod 'Curve25519Kit', :inhibit_warnings => true
pod 'PromiseKit', :inhibit_warnings => true

View File

@ -332,6 +332,6 @@ SPEC CHECKSUMS:
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 2efffb9efc5ed9feaf8b2df90d8dee2d4685e7c2
PODFILE CHECKSUM: 278b25019daa575575de0bf9baf371f7cdcd4fc4
COCOAPODS: 1.10.0.rc.1

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
// TODO: Implementation

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
// TODO: Implementation

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public final class JobQueue : JobDelegate {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public var delegate: JobDelegate?

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public var delegate: JobDelegate?

View File

@ -1,6 +1,6 @@
import PromiseKit
import SessionSnodeKit
import SessionUtilities
import SessionUtilitiesKit
public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public var delegate: JobDelegate?

View File

@ -1,5 +1,5 @@
import SessionProtocolKit
import SessionUtilities
import SessionUtilitiesKit
@objc(SNClosedGroupUpdate)
public final class ClosedGroupUpdate : ControlMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
@objc(SNExpirationTimerUpdate)
public final class ExpirationTimerUpdate : ControlMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
@objc(SNReadReceipt)
public final class ReadReceipt : ControlMessage {

View File

@ -1,5 +1,5 @@
import SessionProtocolKit
import SessionUtilities
import SessionUtilitiesKit
@objc(SNSessionRequest)
public final class SessionRequest : ControlMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
@objc(SNTypingIndicator)
public final class TypingIndicator : ControlMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public extension VisibleMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public extension VisibleMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public extension VisibleMessage {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
@objc(SNVisibleMessage)
public final class VisibleMessage : Message {

View File

@ -1,6 +1,6 @@
import CryptoSwift
import SessionProtocolKit
import SessionUtilities
import SessionUtilitiesKit
internal extension MessageReceiver {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
internal enum MessageReceiver {

View File

@ -1,5 +1,5 @@
import SessionProtocolKit
import SessionUtilities
import SessionUtilitiesKit
internal extension MessageSender {

View File

@ -1,6 +1,6 @@
import PromiseKit
import SessionSnodeKit
import SessionUtilities
import SessionUtilitiesKit
internal enum MessageSender {

View File

@ -1,5 +1,5 @@
import SessionSnodeKit
import SessionUtilities
import SessionUtilitiesKit
public enum MessageWrapper {

View File

@ -1,5 +1,5 @@
import SessionSnodeKit
import SessionUtilities
import SessionUtilitiesKit
enum ProofOfWork {

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public final class ClosedGroupRatchet : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public let chainKey: String

View File

@ -1,6 +1,6 @@
import CryptoSwift
import PromiseKit
import SessionUtilities
import SessionUtilitiesKit
public protocol SharedSenderKeysDelegate {

View File

@ -1,6 +1,6 @@
import CryptoSwift
import Curve25519Kit
import SessionUtilities
import SessionUtilitiesKit
/// A fallback session cipher which uses the the recipient's public key to encrypt data.
@objc public final class FallBackSessionCipher : NSObject {

View File

@ -1,6 +1,6 @@
import CryptoSwift
import PromiseKit
import SessionUtilities
import SessionUtilitiesKit
internal extension OnionRequestAPI {

View File

@ -1,6 +1,6 @@
import CryptoSwift
import PromiseKit
import SessionUtilities
import SessionUtilitiesKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI {

View File

@ -1,5 +1,5 @@
import PromiseKit
import SessionUtilities
import SessionUtilitiesKit
public enum SnodeAPI {

View File

@ -1,5 +1,5 @@
import PromiseKit
import SessionUtilities
import SessionUtilitiesKit
public final class SnodeMessage : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
/// The hex encoded public key of the recipient.

View File

@ -1,4 +1,4 @@
import SessionUtilities
import SessionUtilitiesKit
public protocol SessionSnodeKitStorageProtocol {

View File

@ -1,9 +0,0 @@
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT double SessionUtilitiesVersionNumber;
FOUNDATION_EXPORT const unsigned char SessionUtilitiesVersionString[];
#import <SessionUtilities/ECKeyPair+Utilities.h>
#import <SessionUtilities/NSDate+Timestamp.h>
#import <SessionUtilities/NSTimer+Proxying.h>
#import <SessionUtilities/TSRequest.h>

View File

@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT double SessionUtilitiesKitVersionNumber;
FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
#import <SessionUtilitiesKit/ECKeyPair+Utilities.h>
#import <SessionUtilitiesKit/NSDate+Timestamp.h>
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
#import <SessionUtilitiesKit/TSRequest.h>

View File

@ -24,8 +24,7 @@ public enum Mnemonic {
if let cachedResult = Language.wordSetCache[self] {
return cachedResult
} else {
let bundleID = "org.cocoapods.SessionServiceKit"
let url = Bundle(identifier: bundleID)!.url(forResource: filename, withExtension: "txt")!
let url = Bundle.main.url(forResource: filename, withExtension: "txt")!
let contents = try! String(contentsOf: url)
let result = contents.split(separator: ",").map { String($0) }
Language.wordSetCache[self] = result

View File

@ -470,7 +470,7 @@
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
7BF3FF002505B8E400609570 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */; };
9C9B845C8451114076E55902 /* Pods_SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 686875887229AB29C07145BA /* Pods_SessionUtilities.framework */; };
945AA2B82B621254F69FA9E8 /* Pods_SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */; };
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */; };
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
@ -652,7 +652,8 @@
C3A71D682558A0170043A11F /* LokiSessionCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D652558A0170043A11F /* LokiSessionCipher.swift */; };
C3A71D742558A0F60043A11F /* SMKProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D722558A0F60043A11F /* SMKProto.swift */; };
C3A71D752558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */; };
C3A71D862558A28A0043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE07F2554CDD70050F1E3 /* Storage.swift */; };
@ -679,10 +680,10 @@
C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; };
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; };
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; };
C3C2A67D255388CC00C340D1 /* SessionUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3C2A680255388CC00C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
C3C2A681255388CC00C340D1 /* SessionUtilities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C3C2A6C62553896A00C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3C2A680255388CC00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
C3C2A6F725539DE700C340D1 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -794,7 +795,7 @@
C3C2ABF82553C8A300C340D1 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2ABF72553C8A300C340D1 /* Storage.swift */; };
C3C2AC0A2553C9A100C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC092553C9A100C340D1 /* Configuration.swift */; };
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */; };
C3C2AC372553CCE600C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
C3C2AC372553CCE600C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
C3C3CF8924D8EED300E1CCE7 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C3CF8824D8EED300E1CCE7 /* TextView.swift */; };
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
@ -926,7 +927,7 @@
dstSubfolderSpec = 10;
files = (
C3C2A86A2553B41A00C340D1 /* SessionProtocolKit.framework in Embed Frameworks */,
C3C2A681255388CC00C340D1 /* SessionUtilities.framework in Embed Frameworks */,
C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */,
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */,
4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */,
C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */,
@ -945,6 +946,7 @@
1CE3CD5C23334683BDD3D78C /* Pods-Signal.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.test.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.test.xcconfig"; sourceTree = "<group>"; };
2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionProtocolKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2400888D239F30A600305217 /* SessionRestorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRestorationView.swift; sourceTree = "<group>"; };
264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilitiesKit/Pods-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3303495F6651CE2F3CC9693B /* Pods-SessionUtilities.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilities.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilities/Pods-SessionUtilities.app store release.xcconfig"; sourceTree = "<group>"; };
3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareButtonsView.m; sourceTree = "<group>"; };
@ -1476,7 +1478,6 @@
4CFD151C22415AA400F2450F /* CallVideoHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoHintView.swift; sourceTree = "<group>"; };
4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LegacyNotificationsAdaptee.swift; path = UserInterface/Notifications/LegacyNotificationsAdaptee.swift; sourceTree = "<group>"; };
4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = "<group>"; };
686875887229AB29C07145BA /* Pods_SessionUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
6A26D6558DE69AF455E571C1 /* Pods-SessionMessagingKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.debug.xcconfig"; sourceTree = "<group>"; };
70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
@ -1491,8 +1492,10 @@
7BDCFC0424206E7300641C39 /* LokiPushNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LokiPushNotificationService.entitlements; sourceTree = "<group>"; };
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; };
7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderIcon.swift; sourceTree = "<group>"; };
7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilitiesKit/Pods-SessionUtilitiesKit.app store release.xcconfig"; sourceTree = "<group>"; };
8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = "<group>"; };
8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = "<group>"; };
9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = "<group>"; };
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9B533A9FA46206D3D99C9ADA /* Pods-SignalMessaging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.debug.xcconfig"; sourceTree = "<group>"; };
@ -1689,6 +1692,7 @@
C3A71D662558A0170043A11F /* DiffieHellman.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffieHellman.swift; sourceTree = "<group>"; };
C3A71D722558A0F60043A11F /* SMKProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SMKProto.swift; path = Protos/SMKProto.swift; sourceTree = "<group>"; };
C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSUnidentifiedDelivery.pb.swift; path = Protos/OWSUnidentifiedDelivery.pb.swift; sourceTree = "<group>"; };
C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; };
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; sourceTree = "<group>"; };
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = "<group>"; };
C3BBE0752554CDA60050F1E3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
@ -1719,8 +1723,8 @@
C3C2A5D72553860B00C340D1 /* AESGCM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AESGCM.swift; sourceTree = "<group>"; };
C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
C3C2A5D92553860B00C340D1 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
C3C2A679255388CC00C340D1 /* SessionUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C3C2A67B255388CC00C340D1 /* SessionUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilities.h; sourceTree = "<group>"; };
C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilitiesKit.h; sourceTree = "<group>"; };
C3C2A67C255388CC00C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionMessagingKit.h; sourceTree = "<group>"; };
@ -1901,7 +1905,7 @@
buildActionMask = 2147483647;
files = (
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */,
C3C2A6C62553896A00C340D1 /* SessionUtilities.framework in Frameworks */,
C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1909,7 +1913,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9C9B845C8451114076E55902 /* Pods_SessionUtilities.framework in Frameworks */,
945AA2B82B621254F69FA9E8 /* Pods_SessionUtilitiesKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1928,7 +1932,7 @@
buildActionMask = 2147483647;
files = (
A33A4BA9D050805FE156E3ED /* Pods_SessionProtocolKit.framework in Frameworks */,
C3C2AC372553CCE600C340D1 /* SessionUtilities.framework in Frameworks */,
C3C2AC372553CCE600C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1959,7 +1963,7 @@
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */,
A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */,
A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */,
C3C2A680255388CC00C340D1 /* SessionUtilities.framework in Frameworks */,
C3C2A680255388CC00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */,
D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */,
D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */,
@ -2979,6 +2983,8 @@
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */,
AEA8083C060FF9BAFF6E0C9F /* Pods-SessionProtocolKit.debug.xcconfig */,
174BD0AE74771D02DAC2B7A9 /* Pods-SessionProtocolKit.app store release.xcconfig */,
264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */,
7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
@ -3421,7 +3427,7 @@
path = Utilities;
sourceTree = "<group>";
};
C3C2A67A255388CC00C340D1 /* SessionUtilities */ = {
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */ = {
isa = PBXGroup;
children = (
C3C2A68B255388D500C340D1 /* Meta */,
@ -3435,6 +3441,7 @@
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
C3C2A5D92553860B00C340D1 /* JSON.swift */,
C3C2A5CE2553860700C340D1 /* Logging.swift */,
C3A71F882558BA9F0043A11F /* Mnemonic.swift */,
C300A6302554B68200555489 /* NSDate+Timestamp.h */,
C300A6312554B6D100555489 /* NSDate+Timestamp.mm */,
C352A3762557859C00338F3E /* NSTimer+Proxying.h */,
@ -3444,13 +3451,13 @@
C352A3A42557B5F000338F3E /* TSRequest.h */,
C352A3A52557B60D00338F3E /* TSRequest.m */,
);
path = SessionUtilities;
path = SessionUtilitiesKit;
sourceTree = "<group>";
};
C3C2A68B255388D500C340D1 /* Meta */ = {
isa = PBXGroup;
children = (
C3C2A67B255388CC00C340D1 /* SessionUtilities.h */,
C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */,
C3C2A67C255388CC00C340D1 /* Info.plist */,
);
path = Meta;
@ -3702,7 +3709,7 @@
C3C2A6F125539DE700C340D1 /* SessionMessagingKit */,
C3C2A8632553B41A00C340D1 /* SessionProtocolKit */,
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
C3C2A67A255388CC00C340D1 /* SessionUtilities */,
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */,
D221A08C169C9E5E00537ABF /* Frameworks */,
D221A08A169C9E5E00537ABF /* Products */,
9404664EC513585B05DF1350 /* Pods */,
@ -3718,7 +3725,7 @@
453518921FC63DBF00210559 /* SignalMessaging.framework */,
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */,
C3C2A679255388CC00C340D1 /* SessionUtilities.framework */,
C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */,
C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */,
C3C2A8622553B41A00C340D1 /* SessionProtocolKit.framework */,
);
@ -3769,9 +3776,9 @@
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */,
686875887229AB29C07145BA /* Pods_SessionUtilities.framework */,
FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */,
2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */,
9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -3905,7 +3912,7 @@
C300A63B2554B72200555489 /* NSDate+Timestamp.h in Headers */,
C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */,
C3471F5625553E1100297E91 /* ECKeyPair+Utilities.h in Headers */,
C3C2A67D255388CC00C340D1 /* SessionUtilities.h in Headers */,
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -4051,9 +4058,9 @@
productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */;
productType = "com.apple.product-type.framework";
};
C3C2A678255388CC00C340D1 /* SessionUtilities */ = {
C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */ = {
isa = PBXNativeTarget;
buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilities" */;
buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */;
buildPhases = (
83DABC75697364620557C68B /* [CP] Check Pods Manifest.lock */,
C3C2A674255388CC00C340D1 /* Headers */,
@ -4065,9 +4072,9 @@
);
dependencies = (
);
name = SessionUtilities;
name = SessionUtilitiesKit;
productName = SessionUtilities;
productReference = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */;
productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */;
productType = "com.apple.product-type.framework";
};
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */ = {
@ -4299,7 +4306,7 @@
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */,
C3C2A8612553B41A00C340D1 /* SessionProtocolKit */,
C3C2A59E255385C100C340D1 /* SessionSnodeKit */,
C3C2A678255388CC00C340D1 /* SessionUtilities */,
C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */,
);
};
/* End PBXProject section */
@ -4708,7 +4715,7 @@
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-SessionUtilities-checkManifestLockResult.txt",
"$(DERIVED_FILE_DIR)/Pods-SessionUtilitiesKit-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -5017,15 +5024,16 @@
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */,
C3471F6825553E7600297E91 /* ECKeyPair+Utilities.m in Sources */,
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
C300A60D2554B31900555489 /* Logging.swift in Sources */,
C3A71D862558A28A0043A11F /* DiffieHellman.swift in Sources */,
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -5477,7 +5485,7 @@
};
C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C3C2A678255388CC00C340D1 /* SessionUtilities */;
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
targetProxy = C3C2A67E255388CC00C340D1 /* PBXContainerItemProxy */;
};
C3C2A6F625539DE700C340D1 /* PBXTargetDependency */ = {
@ -6015,7 +6023,7 @@
};
C3C2A682255388CC00C340D1 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E7E2FBF1546840C91B7E4879 /* Pods-SessionUtilities.debug.xcconfig */;
baseConfigurationReference = 264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -6047,7 +6055,7 @@
GCC_OPTIMIZATION_LEVEL = 0;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionUtilities/Meta/Info.plist;
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@ -6062,7 +6070,7 @@
"-framework",
"\"UIKit\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilities";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@ -6076,7 +6084,7 @@
};
C3C2A683255388CC00C340D1 /* App Store Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3303495F6651CE2F3CC9693B /* Pods-SessionUtilities.app store release.xcconfig */;
baseConfigurationReference = 7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -6129,7 +6137,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionUtilities/Meta/Info.plist;
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@ -6144,7 +6152,7 @@
"-framework",
"\"UIKit\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilities";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@ -6854,7 +6862,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = "App Store Release";
};
C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilities" */ = {
C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C3C2A682255388CC00C340D1 /* Debug */,

View File

@ -1,109 +0,0 @@
import CryptoSwift
private extension UInt64 {
fileprivate init(_ decimal: Decimal) {
self.init(truncating: decimal as NSDecimalNumber)
}
// Convert a UInt8 array to a UInt64
fileprivate init(_ bytes: [UInt8]) {
precondition(bytes.count <= MemoryLayout<UInt64>.size)
var value: UInt64 = 0
for byte in bytes {
value <<= 8
value |= UInt64(byte)
}
self.init(value)
}
}
private extension MutableCollection where Element == UInt8, Index == Int {
/// Increment every element by the given amount
///
/// - Parameter amount: The amount to increment by
/// - Returns: The incremented collection
fileprivate func increment(by amount: Int) -> Self {
var result = self
var increment = amount
for i in (0..<result.count).reversed() {
guard increment > 0 else { break }
let sum = Int(result[i]) + increment
result[i] = UInt8(sum % 256)
increment = sum / 256
}
return result
}
}
/**
* The main proof of work logic.
*
* This was copied from the desktop messenger.
* Ref: libloki/proof-of-work.js
*/
public enum ProofOfWork {
// If this changes then we also have to use something other than UInt64 to support the new length
private static let nonceLength = 8
/// Calculate a proof of work with the given configuration
///
/// Ref: https://bitmessage.org/wiki/Proof_of_work
///
/// - Parameters:
/// - data: The message data
/// - pubKey: The message recipient
/// - timestamp: The timestamp
/// - ttl: The message time to live in milliseconds
/// - Returns: A nonce string or `nil` if it failed
public static func calculate(data: String, pubKey: String, timestamp: UInt64, ttl: UInt64) -> String? {
let payload = createPayload(pubKey: pubKey, data: data, timestamp: timestamp, ttl: ttl)
let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(SnodeAPI.powDifficulty))
// Start with the max value
var trialValue = UInt64.max
let initialHash = payload.sha512()
var nonce = [UInt8](repeating: 0, count: nonceLength)
while trialValue > target {
nonce = nonce.increment(by: 1)
// This is different to the bitmessage PoW
// resultHash = hash(nonce + hash(data)) ==> hash(nonce + initialHash)
let resultHash = (nonce + initialHash).sha512()
let trialValueArray = Array(resultHash[0..<8])
trialValue = UInt64(trialValueArray)
}
return nonce.toBase64()
}
/// Get the proof of work payload
private static func createPayload(pubKey: String, data: String, timestamp: UInt64, ttl: UInt64) -> [UInt8] {
let timestampString = String(timestamp)
let ttlString = String(ttl)
let payloadString = timestampString + ttlString + pubKey + data
return payloadString.bytes
}
/// Calculate the target we need to reach
private static func calcTarget(ttl: UInt64, payloadLength: Int, nonceTrials: Int) -> UInt64 {
let two16 = UInt64(pow(2, 16) - 1)
let two64 = UInt64(pow(2, 64) - 1)
// Do all the calculations
let totalLength = UInt64(payloadLength + nonceLength)
let ttlInSeconds = ttl / 1000
let ttlMult = ttlInSeconds * totalLength
// UInt64 values
let innerFrac = ttlMult / two16
let lenPlusInnerFrac = totalLength + innerFrac
let denominator = UInt64(nonceTrials) * lenPlusInnerFrac
return two64 / denominator
}
}

View File

@ -1,75 +0,0 @@
import PromiseKit
public struct LokiMessage {
/// The hex encoded public key of the recipient.
let recipientPublicKey: String
/// The content of the message.
let data: LosslessStringConvertible
/// The time to live for the message in milliseconds.
let ttl: UInt64
/// Whether this message is a ping.
///
/// - Note: The concept of pinging only applies to P2P messaging.
let isPing: Bool
/// When the proof of work was calculated, if applicable (P2P messages don't require proof of work).
///
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
private(set) var timestamp: UInt64? = nil
/// The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work).
private(set) var nonce: String? = nil
private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) {
self.recipientPublicKey = destination
self.data = data
self.ttl = ttl
self.isPing = isPing
}
/// Construct a `LokiMessage` from a `SignalMessage`.
///
/// - Note: `timestamp` is the original message timestamp (i.e. `TSOutgoingMessage.timestamp`).
public static func from(signalMessage: SignalMessage) -> LokiMessage? {
// To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object
do {
let wrappedMessage = try MessageWrapper.wrap(message: signalMessage)
let data = wrappedMessage.base64EncodedString()
let destination = signalMessage.recipientPublicKey
var ttl = TTLUtilities.fallbackMessageTTL
if let messageTTL = signalMessage.ttl, messageTTL > 0 { ttl = UInt64(messageTTL) }
let isPing = signalMessage.isPing
return LokiMessage(destination: destination, data: data, ttl: ttl, isPing: isPing)
} catch let error {
print("[Loki] Failed to convert Signal message to Loki message: \(signalMessage).")
return nil
}
}
/// Calculate the proof of work for this message.
///
/// - Returns: The promise of a new message with its `timestamp` and `nonce` set.
public func calculatePoW() -> Promise<LokiMessage> {
return Promise<LokiMessage> { seal in
DispatchQueue.global(qos: .userInitiated).async {
let now = NSDate.ows_millisecondTimeStamp()
let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.recipientPublicKey, timestamp: now, ttl: self.ttl) {
var result = self
result.timestamp = now
result.nonce = nonce
seal.fulfill(result)
} else {
seal.reject(SnodeAPI.SnodeAPIError.proofOfWorkCalculationFailed)
}
}
}
}
public func toJSON() -> JSON {
var result = [ "pubKey" : recipientPublicKey, "data" : data.description, "ttl" : String(ttl) ]
if let timestamp = timestamp, let nonce = nonce {
result["timestamp"] = String(timestamp)
result["nonce"] = nonce
}
return result
}
}

View File

@ -1,72 +0,0 @@
public enum MessageWrapper {
public enum Error : LocalizedError {
case failedToWrapData
case failedToWrapMessageInEnvelope
case failedToWrapEnvelopeInWebSocketMessage
case failedToUnwrapData
public var errorDescription: String? {
switch self {
case .failedToWrapData: return "Failed to wrap data."
case .failedToWrapMessageInEnvelope: return "Failed to wrap message in envelope."
case .failedToWrapEnvelopeInWebSocketMessage: return "Failed to wrap envelope in web socket message."
case .failedToUnwrapData: return "Failed to unwrap data."
}
}
}
/// Wraps `message` in an `SSKProtoEnvelope` and then a `WebSocketProtoWebSocketMessage` to match the desktop application.
public static func wrap(message: SignalMessage) throws -> Data {
do {
let envelope = try createEnvelope(around: message)
let webSocketMessage = try createWebSocketMessage(around: envelope)
return try webSocketMessage.serializedData()
} catch let error {
throw error as? Error ?? Error.failedToWrapData
}
}
private static func createEnvelope(around message: SignalMessage) throws -> SSKProtoEnvelope {
do {
let builder = SSKProtoEnvelope.builder(type: message.type, timestamp: message.timestamp)
builder.setSource(message.senderPublicKey)
builder.setSourceDevice(message.senderDeviceID)
if let content = try Data(base64Encoded: message.content, options: .ignoreUnknownCharacters) {
builder.setContent(content)
} else {
throw Error.failedToWrapMessageInEnvelope
}
return try builder.build()
} catch let error {
print("[Loki] Failed to wrap message in envelope: \(error).")
throw Error.failedToWrapMessageInEnvelope
}
}
private static func createWebSocketMessage(around envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage {
do {
let requestBuilder = WebSocketProtoWebSocketRequestMessage.builder(verb: "PUT", path: "/api/v1/message", requestID: UInt64.random(in: 1..<UInt64.max))
requestBuilder.setBody(try envelope.serializedData())
let messageBuilder = WebSocketProtoWebSocketMessage.builder(type: .request)
messageBuilder.setRequest(try requestBuilder.build())
return try messageBuilder.build()
} catch let error {
print("[Loki] Failed to wrap envelope in web socket message: \(error).")
throw Error.failedToWrapEnvelopeInWebSocketMessage
}
}
/// - Note: `data` shouldn't be base 64 encoded.
public static func unwrap(data: Data) throws -> SSKProtoEnvelope {
do {
let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data)
let envelope = webSocketMessage.request!.body!
return try SSKProtoEnvelope.parseData(envelope)
} catch let error {
print("[Loki] Failed to unwrap data: \(error).")
throw Error.failedToUnwrapData
}
}
}

View File

@ -1,72 +0,0 @@
import CryptoSwift
import PromiseKit
extension OnionRequestAPI {
internal static func encode(ciphertext: Data, json: JSON) throws -> Data {
// The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
guard JSONSerialization.isValidJSONObject(json) else { throw HTTP.Error.invalidJSON }
let jsonAsData = try JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ])
let ciphertextSize = Int32(ciphertext.count).littleEndian
let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout<Int32>.size) }
return ciphertextSizeAsData + ciphertext + jsonAsData
}
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
// Wrapping isn't needed for file server or open group onion requests
switch destination {
case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
let result = try EncryptionUtilities.encrypt(plaintext, using: snodeX25519PublicKey)
seal.fulfill(result)
case .server(_, let serverX25519PublicKey):
let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let result = try EncryptionUtilities.encrypt(plaintext, using: serverX25519PublicKey)
seal.fulfill(result)
}
} catch (let error) {
seal.reject(error)
}
}
return promise
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
internal static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
var parameters: JSON
switch rhs {
case .snode(let snode):
guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, _):
parameters = [ "host" : host, "target" : "/loki/v2/lsrpc", "method" : "POST" ]
}
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String
switch lhs {
case .snode(let snode):
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
x25519PublicKey = snodeX25519PublicKey
case .server(_, let serverX25519PublicKey):
x25519PublicKey = serverX25519PublicKey
}
do {
let plaintext = try encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
let result = try EncryptionUtilities.encrypt(plaintext, using: x25519PublicKey)
seal.fulfill(result)
} catch (let error) {
seal.reject(error)
}
}
return promise
}
}

View File

@ -1,435 +0,0 @@
import CryptoSwift
import PromiseKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI {
private static var pathFailureCount: [Path:UInt] = [:]
private static var snodeFailureCount: [Snode:UInt] = [:]
public static var guardSnodes: Set<Snode> = []
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
// MARK: Settings
/// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 3
/// The number of times a path can fail before it's replaced.
private static let pathFailureThreshold: UInt = 3
/// The number of times a snode can fail before it's replaced.
private static let snodeFailureThreshold: UInt = 3
/// The number of paths to maintain.
public static let targetPathCount: UInt = 2
/// The number of guard snodes required to maintain `targetPathCount` paths.
private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path
// MARK: Destination
internal enum Destination {
case snode(Snode)
case server(host: String, x25519PublicKey: String)
}
// MARK: Error
public enum Error : LocalizedError {
case httpRequestFailedAtDestination(statusCode: UInt, json: JSON)
case insufficientSnodes
case invalidURL
case missingSnodeVersion
case snodePublicKeySetMissing
case unsupportedSnodeVersion(String)
public var errorDescription: String? {
switch self {
case .httpRequestFailedAtDestination(let statusCode): return "HTTP request failed at destination with status code: \(statusCode)."
case .insufficientSnodes: return "Couldn't find enough snodes to build a path."
case .invalidURL: return "Invalid URL"
case .missingSnodeVersion: return "Missing snode version."
case .snodePublicKeySetMissing: return "Missing snode public key set."
case .unsupportedSnodeVersion(let version): return "Unsupported snode version: \(version)."
}
}
}
// MARK: Path
public typealias Path = [Snode]
// MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, destinationSymmetricKey: Data)
// MARK: Private API
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
private static func testSnode(_ snode: Snode) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
HTTP.execute(.get, url, timeout: timeout).done2 { rawResponse in
guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
if version >= "2.0.7" {
seal.fulfill(())
} else {
print("[Loki] [Onion Request API] Unsupported snode version: \(version).")
seal.reject(Error.unsupportedSnodeVersion(version))
}
}.catch2 { error in
seal.reject(error)
}
}
return promise
}
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise<Set<Snode>> {
if guardSnodes.count >= targetGuardSnodeCount {
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
} else {
print("[Loki] [Onion Request API] Populating guard snode cache.")
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool
var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { throw Error.insufficientSnodes }
func getGuardSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } }
unusedSnodes.remove(candidate) // All used snodes should be unique
print("[Loki] [Onion Request API] Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
withDelay(0.1, completionQueue: SnodeAPI.workQueue) { getGuardSnode() }
}
}
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() }
return when(fulfilled: promises).map2 { guardSnodes in
let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes)
OnionRequestAPI.guardSnodes = guardSnodesAsSet
return guardSnodesAsSet
}
}
}
}
/// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> {
print("[Loki] [Onion Request API] Building onion request paths.")
DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil)
}
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
let reusableGuardSnodes = reusablePaths.map { $0[0] }
return getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in
var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 })
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them
return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
// randomElement() uses the system's default random generator, which is cryptographically secure
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
unusedSnodes.remove(pathSnode) // All used snodes should be unique
return pathSnode
}
print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).")
return result
}
}.map2 { paths in
OnionRequestAPI.paths = paths + reusablePaths
Storage.writeSync { transaction in
print("[Loki] Persisting onion request paths to database.")
Storage.setOnionRequestPaths(paths, using: transaction)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
return paths
}
}
}
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
private static func getPath(excluding snode: Snode?) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
var paths = OnionRequestAPI.paths
if paths.isEmpty {
paths = Storage.getOnionRequestPaths()
OnionRequestAPI.paths = paths
if !paths.isEmpty {
guardSnodes.formUnion([ paths[0][0] ])
if paths.count >= 2 {
guardSnodes.formUnion([ paths[1][0] ])
}
}
}
// randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= targetPathCount {
if let snode = snode {
return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) }
} else {
return Promise { $0.fulfill(paths.randomElement()!) }
}
} else if !paths.isEmpty {
if let snode = snode {
if let path = paths.first(where: { !$0.contains(snode) }) {
buildPaths(reusing: paths).retainUntilComplete() // Re-build paths in the background
return Promise { $0.fulfill(path) }
} else {
return buildPaths(reusing: paths).map2 { paths in
return paths.filter { !$0.contains(snode) }.randomElement()!
}
}
} else {
buildPaths(reusing: paths).retainUntilComplete() // Re-build paths in the background
return Promise { $0.fulfill(paths.randomElement()!) }
}
} else {
return buildPaths(reusing: []).map2 { paths in
if let snode = snode {
return paths.filter { !$0.contains(snode) }.randomElement()!
} else {
return paths.randomElement()!
}
}
}
}
private static func dropGuardSnode(_ snode: Snode) {
guardSnodes = guardSnodes.filter { $0 != snode }
}
private static func drop(_ snode: Snode) throws {
// We repair the path here because we can do it sync. In the case where we drop a whole
// path we leave the re-building up to getPath(excluding:) because re-building the path
// in that case is async.
OnionRequestAPI.snodeFailureCount[snode] = 0
var oldPaths = paths
guard let pathIndex = oldPaths.firstIndex(where: { $0.contains(snode) }) else { return }
var path = oldPaths[pathIndex]
guard let snodeIndex = path.firstIndex(of: snode) else { return }
path.remove(at: snodeIndex)
let unusedSnodes = SnodeAPI.snodePool.subtracting(oldPaths.flatMap { $0 })
guard !unusedSnodes.isEmpty else { throw Error.insufficientSnodes }
// randomElement() uses the system's default random generator, which is cryptographically secure
path.append(unusedSnodes.randomElement()!)
// Don't test the new snode as this would reveal the user's IP
oldPaths.remove(at: pathIndex)
let newPaths = oldPaths + [ path ]
paths = newPaths
Storage.writeSync { transaction in
print("[Loki] Persisting onion request paths to database.")
Storage.setOnionRequestPaths(newPaths, using: transaction)
}
}
private static func drop(_ path: Path) {
OnionRequestAPI.pathFailureCount[path] = 0
var paths = OnionRequestAPI.paths
guard let pathIndex = paths.firstIndex(of: path) else { return }
paths.remove(at: pathIndex)
OnionRequestAPI.paths = paths
Storage.writeSync { transaction in
if !paths.isEmpty {
print("[Loki] Persisting onion request paths to database.")
Storage.setOnionRequestPaths(paths, using: transaction)
} else {
Storage.clearOnionRequestPaths(using: transaction)
}
}
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: EncryptionResult!
var snodeToExclude: Snode?
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude).then2 { path -> Promise<EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination).then2 { r -> Promise<EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var rhs = destination
func addLayer() -> Promise<EncryptionResult> {
if path.isEmpty {
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
let lhs = Destination.snode(path.removeLast())
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in
encryptionResult = r
rhs = lhs
return addLayer()
}
}
}
return addLayer()
}
}.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
}
// MARK: Internal API
/// Sends an onion request to `snode`. Builds new paths as needed.
internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<JSON> in
guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
}
/// Sends an onion request to `server`. Builds new paths as needed.
internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519PublicKey: String, isJSONRequired: Bool = true) -> Promise<JSON> {
var rawHeaders = request.allHTTPHeaderFields ?? [:]
rawHeaders.removeValue(forKey: "User-Agent")
var headers: JSON = rawHeaders.mapValues { value in
switch value.lowercased() {
case "true": return true
case "false": return false
default: return value
}
}
guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
var endpoint = ""
if server.count < url.count {
guard let serverEndIndex = url.range(of: server)?.upperBound else { return Promise(error: Error.invalidURL) }
let endpointStartIndex = url.index(after: serverEndIndex)
endpoint = String(url[endpointStartIndex..<url.endIndex])
}
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"
let tsRequestParameters = tsRequest.parameters
if !tsRequestParameters.isEmpty {
guard let parameters = try? JSONSerialization.data(withJSONObject: tsRequestParameters, options: [ .fragmentsAllowed ]) else {
return Promise(error: HTTP.Error.invalidJSON)
}
parametersAsString = String(bytes: parameters, encoding: .utf8) ?? "null"
} else {
parametersAsString = "null"
}
} else {
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
if let parametersAsInputStream = request.httpBodyStream, let parameters = try? Data(from: parametersAsInputStream) {
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parameters.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
} else {
parametersAsString = "null"
}
}
let payload: JSON = [
"body" : parametersAsString,
"endpoint" : endpoint,
"method" : request.httpMethod!,
"headers" : headers
]
let destination = Destination.server(host: host, x25519PublicKey: x25519PublicKey)
let promise = sendOnionRequest(with: payload, to: destination, isJSONRequired: isJSONRequired)
promise.catch2 { error in
print("[Loki] [Onion Request API] Couldn't reach server: \(url) due to error: \(error).")
}
return promise
}
internal static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: Snode!
SnodeAPI.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req/v2"
let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext
if case Destination.server = destination, Double(onion.count) > 0.75 * Double(FileServerAPI.maxFileSize) {
print("[Loki] Approaching request size limit: ~\(onion.count) bytes.")
}
let parameters: JSON = [
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
]
let body: Data
do {
body = try encode(ciphertext: onion, json: parameters)
} catch {
return seal.reject(error)
}
let destinationSymmetricKey = intermediate.destinationSymmetricKey
HTTP.execute(.post, url, body: body).done2 { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String,
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= EncryptionUtilities.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
do {
let data = try DecryptionUtilities.decrypt(ivAndCiphertext, usingAESGCMWithSymmetricKey: destinationSymmetricKey)
guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON,
let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
if statusCode == 406 { // Clock out of sync
print("[Loki] The user's clock is out of sync with the service node network.")
seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync)
} else if let bodyAsString = json["body"] as? String {
let body: JSON
if !isJSONRequired {
body = [ "result" : bodyAsString ]
} else {
guard let bodyAsData = bodyAsString.data(using: .utf8),
let b = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
body = b
}
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body)) }
seal.fulfill(body)
} else {
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json)) }
seal.fulfill(json)
}
} catch {
seal.reject(error)
}
}.catch2 { error in
seal.reject(error)
}
}.catch2 { error in
seal.reject(error)
}
}
promise.catch2 { error in // Must be invoked on LokiAPI.workQueue
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { return }
let path = paths.first { $0.contains(guardSnode) }
func handleUnspecificError() {
guard let path = path else { return }
var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0
pathFailureCount += 1
if pathFailureCount >= pathFailureThreshold {
dropGuardSnode(guardSnode)
path.forEach { snode in
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
}
drop(path)
} else {
OnionRequestAPI.pathFailureCount[path] = pathFailureCount
}
}
let prefix = "Next node not found: "
if let message = json?["result"] as? String, message.hasPrefix(prefix) {
let ed25519PublicKey = message.substring(from: prefix.count)
if let path = path, let snode = path.first(where: { $0.publicKeySet?.ed25519Key == ed25519PublicKey }) {
var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?? 0
snodeFailureCount += 1
if snodeFailureCount >= snodeFailureThreshold {
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
do {
try drop(snode)
} catch {
handleUnspecificError()
}
} else {
OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount
}
} else {
handleUnspecificError()
}
} else if let message = json?["result"] as? String, message == "Loki Server error" {
// Do nothing
} else {
handleUnspecificError()
}
}
return promise
}
}

View File

@ -1,28 +0,0 @@
@objc(LKSignalMessage)
public final class SignalMessage : NSObject {
@objc public let type: SSKProtoEnvelope.SSKProtoEnvelopeType
@objc public let timestamp: UInt64
@objc public let senderPublicKey: String
@objc public let senderDeviceID: UInt32
@objc public let content: String
@objc public let recipientPublicKey: String
@objc(ttl)
public let objc_ttl: UInt64
@objc public let isPing: Bool
public var ttl: UInt64? { return objc_ttl != 0 ? objc_ttl : nil }
@objc public init(type: SSKProtoEnvelope.SSKProtoEnvelopeType, timestamp: UInt64, senderID: String, senderDeviceID: UInt32,
content: String, recipientID: String, ttl: UInt64, isPing: Bool) {
self.type = type
self.timestamp = timestamp
self.senderPublicKey = senderID
self.senderDeviceID = senderDeviceID
self.content = content
self.recipientPublicKey = recipientID
self.objc_ttl = ttl
self.isPing = isPing
super.init()
}
}

View File

@ -1,66 +0,0 @@
public final class Snode : NSObject, NSCoding {
public let address: String
public let port: UInt16
internal let publicKeySet: KeySet?
public var ip: String {
String(address[address.index(address.startIndex, offsetBy: 8)..<address.endIndex])
}
// MARK: Nested Types
internal enum Method : String {
/// Only supported by snode targets.
case getSwarm = "get_snodes_for_pubkey"
/// Only supported by snode targets.
case getMessages = "retrieve"
case sendMessage = "store"
}
internal struct KeySet {
let ed25519Key: String
let x25519Key: String
}
// MARK: Initialization
internal init(address: String, port: UInt16, publicKeySet: KeySet?) {
self.address = address
self.port = port
self.publicKeySet = publicKeySet
}
// MARK: Coding
public init?(coder: NSCoder) {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt16
if let idKey = coder.decodeObject(forKey: "idKey") as? String, let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String {
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
} else {
publicKeySet = nil
}
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(address, forKey: "address")
coder.encode(port, forKey: "port")
if let keySet = publicKeySet {
coder.encode(keySet.ed25519Key, forKey: "idKey")
coder.encode(keySet.x25519Key, forKey: "encryptionKey")
}
}
// MARK: Equality
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? Snode else { return false }
return address == other.address && port == other.port
}
// MARK: Hashing
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
return address.hashValue ^ port.hashValue
}
// MARK: Description
override public var description: String { return "\(address):\(port)" }
}

View File

@ -1,352 +0,0 @@
import PromiseKit
@objc(LKSnodeAPI)
public final class SnodeAPI : NSObject {
internal static let workQueue = DispatchQueue(label: "SnodeAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var snodeFailureCount: [Snode:UInt] = [:]
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var snodePool: Set<Snode> = []
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
internal static var swarmCache: [String:[Snode]] = [:]
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
// MARK: Settings
private static let maxRetryCount: UInt = 4
private static let minimumSnodePoolCount = 64
private static let minimumSwarmSnodeCount = 2
private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
private static let snodeFailureThreshold = 4
private static let targetSwarmSnodeCount = 2
internal static var powDifficulty: UInt = 1
/// - Note: Changing this on the fly is not recommended.
internal static var useOnionRequests = true
// MARK: Error
@objc(LKSnodeAPIError)
public class SnodeAPIError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let proofOfWorkCalculationFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
@objc public static let messageConversionFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
@objc public static let clockOutOfSync = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
@objc public static let randomSnodePoolUpdatingFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
@objc public static let missingSnodeVersion = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Missing service node version." ])
}
// MARK: Type Aliases
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
public typealias RawResponse = Any
public typealias RawResponsePromise = Promise<RawResponse>
// MARK: Lifecycle
override private init() { }
// MARK: Core
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
if useOnionRequests {
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
} else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
}
}
internal static func getRandomSnode() -> Promise<Snode> {
if snodePool.count < minimumSnodePoolCount {
storage.dbReadConnection.read { transaction in
snodePool = storage.getSnodePool(in: transaction)
}
}
if snodePool.count < minimumSnodePoolCount {
let target = seedNodePool.randomElement()!
let url = "\(target)/json_rpc"
let parameters: JSON = [
"method" : "get_n_service_nodes",
"params" : [
"active_only" : true,
"fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
]
]
]
print("[Loki] Populating snode pool using: \(target).")
let (promise, seal) = Promise<Snode>.pending()
attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Snode in
guard let intermediate = json["result"] as? JSON, let rawSnodes = intermediate["service_node_states"] as? [JSON] else { throw SnodeAPIError.randomSnodePoolUpdatingFailed }
snodePool = try Set(rawSnodes.flatMap { rawSnode in
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
// randomElement() uses the system's default random generator, which is cryptographically secure
if !snodePool.isEmpty {
return snodePool.randomElement()!
} else {
throw SnodeAPIError.randomSnodePoolUpdatingFailed
}
}
}.done2 { snode in
seal.fulfill(snode)
Storage.writeSync { transaction in
print("[Loki] Persisting snode pool to database.")
storage.setSnodePool(SnodeAPI.snodePool, in: transaction)
}
}.catch2 { error in
print("[Loki] Failed to contact seed node at: \(target).")
seal.reject(error)
}
return promise
} else {
return Promise<Snode> { seal in
// randomElement() uses the system's default random generator, which is cryptographically secure
seal.fulfill(snodePool.randomElement()!)
}
}
}
internal static func getSwarm(for publicKey: String, isForcedReload: Bool = false) -> Promise<[Snode]> {
if swarmCache[publicKey] == nil {
storage.dbReadConnection.read { transaction in
swarmCache[publicKey] = storage.getSwarm(for: publicKey, in: transaction)
}
}
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
return Promise<[Snode]> { $0.fulfill(cachedSwarm) }
} else {
print("[Loki] Getting swarm for: \(publicKey == getUserHexEncodedPublicKey() ? "self" : publicKey).")
let parameters: [String:Any] = [ "pubKey" : publicKey ]
return getRandomSnode().then2 { snode in
attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters)
}
}.map2 { rawSnodes in
let swarm = parseSnodes(from: rawSnodes)
swarmCache[publicKey] = swarm
Storage.writeSync { transaction in
storage.setSwarm(swarm, for: publicKey, in: transaction)
}
return swarm
}
}
}
internal static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
// shuffled() uses the system's default random generator, which is cryptographically secure
return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
}
internal static func dropSnodeFromSnodePool(_ snode: Snode) {
SnodeAPI.snodePool.remove(snode)
Storage.writeSync { transaction in
storage.dropSnodeFromSnodePool(snode, in: transaction)
}
}
@objc public static func clearSnodePool() {
snodePool.removeAll()
Storage.writeSync { transaction in
storage.clearSnodePool(in: transaction)
}
}
internal static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
let swarm = SnodeAPI.swarmCache[publicKey]
if var swarm = swarm, let index = swarm.firstIndex(of: snode) {
swarm.remove(at: index)
SnodeAPI.swarmCache[publicKey] = swarm
Storage.writeSync { transaction in
storage.setSwarm(swarm, for: publicKey, in: transaction)
}
}
}
// MARK: Receiving
internal static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
Storage.writeSync { transaction in
Storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey, using: transaction)
}
let lastHash = Storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? ""
let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ]
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
}
public static func getMessages(for publicKey: String) -> Promise<Set<MessageListPromise>> {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
getTargetSnodes(for: publicKey).mapValues2 { targetSnode in
getRawMessages(from: targetSnode, associatedWith: publicKey).map2 {
parseRawMessagesResponse($0, from: targetSnode, associatedWith: publicKey)
}
}.map2 { Set($0) }
}
}
// MARK: Sending
@objc(sendSignalMessage:)
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage) -> AnyPromise {
let promise = sendSignalMessage(signalMessage).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) }
return AnyPromise.from(promise)
}
public static func sendSignalMessage(_ signalMessage: SignalMessage) -> Promise<Set<RawResponsePromise>> {
// Convert the message to a Loki message
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: SnodeAPIError.messageConversionFailed) }
let publicKey = lokiMessage.recipientPublicKey
let notificationCenter = NotificationCenter.default
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
// Calculate proof of work
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
// Get the target snodes
return getTargetSnodes(for: publicKey).map2 { snodes in
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
let parameters = lokiMessageWithPoW.toJSON()
return Set(snodes.map { snode in
// Send the message to the target snode
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
invoke(.sendMessage, on: snode, associatedWith: publicKey, parameters: parameters)
}.map2 { rawResponse in
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
guard powDifficulty != SnodeAPI.powDifficulty, powDifficulty < 100 else { return rawResponse }
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
SnodeAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
}
return rawResponse
}
})
}
}
}
// MARK: Parsing
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
private static func parseSnodes(from rawResponse: Any) -> [Snode] {
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
print("[Loki] Failed to parse targets from: \(rawResponse).")
return []
}
return rawSnodes.flatMap { rawSnode in
guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: port, publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
}
}
internal static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [SSKProtoEnvelope] {
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages)
let rawNewMessages = removeDuplicates(from: rawMessages, associatedWith: publicKey)
let newMessages = parseProtoEnvelopes(from: rawNewMessages)
return newMessages
}
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) {
if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
Storage.writeSync { transaction in
Storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey, to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
}
} else if (!rawMessages.isEmpty) {
print("[Loki] Failed to update last message hash value from: \(rawMessages).")
}
}
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
var receivedMessages = Storage.getReceivedMessages(for: publicKey) ?? []
return rawMessages.filter { rawMessage in
guard let hash = rawMessage["hash"] as? String else {
print("[Loki] Missing hash value for message: \(rawMessage).")
return false
}
let isDuplicate = receivedMessages.contains(hash)
receivedMessages.insert(hash)
Storage.writeSync { transaction in
Storage.setReceivedMessages(to: receivedMessages, for: publicKey, using: transaction)
}
return !isDuplicate
}
}
private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
return rawMessages.compactMap { rawMessage in
guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
print("[Loki] Failed to decode data for message: \(rawMessage).")
return nil
}
guard let envelope = try? MessageWrapper.unwrap(data: data) else {
print("[Loki] Failed to unwrap data for message: \(rawMessage).")
return nil
}
return envelope
}
}
// MARK: Error Handling
/// - Note: Should only be invoked from `LokiAPI.workQueue` to avoid race conditions.
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? {
#if DEBUG
assertOnQueue(SnodeAPI.workQueue)
#endif
func handleBadSnode() {
let oldFailureCount = SnodeAPI.snodeFailureCount[snode] ?? 0
let newFailureCount = oldFailureCount + 1
SnodeAPI.snodeFailureCount[snode] = newFailureCount
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
if newFailureCount >= SnodeAPI.snodeFailureThreshold {
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
if let publicKey = publicKey {
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
}
SnodeAPI.dropSnodeFromSnodePool(snode)
print("[Loki] Snode pool count: \(snodePool.count).")
SnodeAPI.snodeFailureCount[snode] = 0
}
}
switch statusCode {
case 0, 400, 500, 503:
// The snode is unreachable
handleBadSnode()
case 406:
print("[Loki] The user's clock is out of sync with the service node network.")
return SnodeAPI.SnodeAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
if let publicKey = publicKey {
print("[Loki] Invalidating swarm for: \(publicKey).")
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
} else {
print("[Loki] Got a 421 without an associated public key.")
}
case 432:
// The proof of work difficulty is too low
if let powDifficulty = json?["difficulty"] as? UInt {
if powDifficulty < 100 {
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
SnodeAPI.powDifficulty = UInt(powDifficulty)
} else {
handleBadSnode()
}
} else {
print("[Loki] Failed to update proof of work difficulty.")
}
default:
handleBadSnode()
print("[Loki] Unhandled response code: \(statusCode).")
}
return nil
}
}

View File

@ -1,18 +0,0 @@
import CryptoSwift
enum DecryptionUtilities {
/// - Note: Sync. Don't call from the main thread.
internal static func decrypt(_ ivAndCiphertext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data {
if Thread.isMainThread {
#if DEBUG
preconditionFailure("It's illegal to call decrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.")
#endif
}
let iv = ivAndCiphertext[0..<Int(EncryptionUtilities.ivSize)]
let ciphertext = ivAndCiphertext[Int(EncryptionUtilities.ivSize)...]
let gcm = GCM(iv: iv.bytes, tagLength: Int(EncryptionUtilities.gcmTagSize), mode: .combined)
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
return Data(try aes.decrypt(ciphertext.bytes))
}
}

View File

@ -1,38 +0,0 @@
import CryptoSwift
internal typealias EncryptionResult = (ciphertext: Data, symmetricKey: Data, ephemeralPublicKey: Data)
enum EncryptionUtilities {
internal static let gcmTagSize: UInt = 16
internal static let ivSize: UInt = 12
/// - Note: Sync. Don't call from the main thread.
internal static func encrypt(_ plaintext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data {
if Thread.isMainThread {
#if DEBUG
preconditionFailure("It's illegal to call encrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.")
#endif
}
let iv = Data.getSecureRandomData(ofSize: ivSize)!
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)
return iv + Data(bytes: ciphertext)
}
/// - Note: Sync. Don't call from the main thread.
internal static func encrypt(_ plaintext: Data, using hexEncodedX25519PublicKey: String) throws -> EncryptionResult {
if Thread.isMainThread {
#if DEBUG
preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.")
#endif
}
let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
let ephemeralKeyPair = Curve25519.generateKeyPair()
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey)
let salt = "LOKI"
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey))
return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey)
}
}

View File

@ -1,107 +0,0 @@
import PromiseKit
public enum HTTP {
private static let seedNodeURLSession = URLSession(configuration: .ephemeral)
private static let defaultURLSession = URLSession(configuration: .ephemeral, delegate: defaultURLSessionDelegate, delegateQueue: nil)
private static let defaultURLSessionDelegate = DefaultURLSessionDelegateImplementation()
// MARK: Settings
public static let timeout: TimeInterval = 10
// MARK: URL Session Delegate Implementation
private final class DefaultURLSessionDelegateImplementation : NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Snode to snode communication uses self-signed certificates but clients can safely ignore this
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
// MARK: Verb
public enum Verb : String {
case get = "GET"
case put = "PUT"
case post = "POST"
case delete = "DELETE"
}
// MARK: Error
public enum Error : LocalizedError {
case generic
case httpRequestFailed(statusCode: UInt, json: JSON?)
case invalidJSON
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
case .invalidJSON: return "Invalid JSON."
}
}
}
// MARK: Main
public static func execute(_ verb: Verb, _ url: String, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
}
public static func execute(_ verb: Verb, _ url: String, parameters: JSON?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
if let parameters = parameters {
do {
guard JSONSerialization.isValidJSONObject(parameters) else { return Promise(error: Error.invalidJSON) }
let body = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ])
return execute(verb, url, body: body, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
} catch (let error) {
return Promise(error: error)
}
} else {
return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
}
}
public static func execute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
request.httpBody = body
request.timeoutInterval = timeout
request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent")
let (promise, seal) = Promise<JSON>.pending()
let urlSession = useSeedNodeURLSession ? seedNodeURLSession : defaultURLSession
let task = urlSession.dataTask(with: request) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse else {
if let error = error {
print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).")
} else {
print("[Loki] \(verb.rawValue) request to \(url) failed.")
}
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
}
if let error = error {
print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).")
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
}
let statusCode = UInt(response.statusCode)
var json: JSON? = nil
if let j = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
json = j
} else if let result = String(data: data, encoding: .utf8) {
json = [ "result" : result ]
}
guard 200...299 ~= statusCode else {
let jsonDescription = json?.prettifiedDescription ?? "no debugging info provided"
print("[Loki] \(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: json))
}
if let json = json {
seal.fulfill(json)
} else {
print("[Loki] Couldn't parse JSON returned by \(verb.rawValue) request to \(url).")
return seal.reject(Error.invalidJSON)
}
}
task.resume()
return promise
}
}

View File

@ -1,44 +0,0 @@
public final class ClosedGroupRatchet : NSObject, NSCoding {
public let chainKey: String
public let keyIndex: UInt
public let messageKeys: [String]
// MARK: Initialization
public init(chainKey: String, keyIndex: UInt, messageKeys: [String]) {
self.chainKey = chainKey
self.keyIndex = keyIndex
self.messageKeys = messageKeys
}
// MARK: Coding
public init?(coder: NSCoder) {
guard let chainKey = coder.decodeObject(forKey: "chainKey") as? String,
let keyIndex = coder.decodeObject(forKey: "keyIndex") as? UInt,
let messageKeys = coder.decodeObject(forKey: "messageKeys") as? [String] else { return nil }
self.chainKey = chainKey
self.keyIndex = UInt(keyIndex)
self.messageKeys = messageKeys
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(chainKey, forKey: "chainKey")
coder.encode(keyIndex, forKey: "keyIndex")
coder.encode(messageKeys, forKey: "messageKeys")
}
// MARK: Equality
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? ClosedGroupRatchet else { return false }
return chainKey == other.chainKey && keyIndex == other.keyIndex && messageKeys == other.messageKeys
}
// MARK: Hashing
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
return chainKey.hashValue ^ keyIndex.hashValue ^ messageKeys.hashValue
}
// MARK: Description
override public var description: String { return "[ chainKey : \(chainKey), keyIndex : \(keyIndex), messageKeys : \(messageKeys.prettifiedDescription) ]" }
}

View File

@ -1,51 +0,0 @@
internal final class ClosedGroupSenderKey : NSObject, NSCoding {
internal let chainKey: Data
internal let keyIndex: UInt
internal let publicKey: Data
// MARK: Initialization
init(chainKey: Data, keyIndex: UInt, publicKey: Data) {
self.chainKey = chainKey
self.keyIndex = keyIndex
self.publicKey = publicKey
}
// MARK: Coding
public init?(coder: NSCoder) {
guard let chainKey = coder.decodeObject(forKey: "chainKey") as? Data,
let keyIndex = coder.decodeObject(forKey: "keyIndex") as? UInt,
let publicKey = coder.decodeObject(forKey: "publicKey") as? Data else { return nil }
self.chainKey = chainKey
self.keyIndex = UInt(keyIndex)
self.publicKey = publicKey
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(chainKey, forKey: "chainKey")
coder.encode(keyIndex, forKey: "keyIndex")
coder.encode(publicKey, forKey: "publicKey")
}
// MARK: Proto Conversion
internal func toProto() throws -> SSKProtoDataMessageClosedGroupUpdateSenderKey {
return try SSKProtoDataMessageClosedGroupUpdateSenderKey.builder(chainKey: chainKey, keyIndex: UInt32(keyIndex), publicKey: publicKey).build()
}
// MARK: Equality
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? ClosedGroupSenderKey else { return false }
return chainKey == other.chainKey && keyIndex == other.keyIndex && publicKey == other.publicKey
}
// MARK: Hashing
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
return chainKey.hashValue ^ keyIndex.hashValue ^ publicKey.hashValue
}
// MARK: Description
override public var description: String {
return "[ chainKey : \(chainKey), keyIndex : \(keyIndex), publicKey: \(publicKey.toHexString()) ]"
}
}

View File

@ -1,125 +0,0 @@
@objc(LKClosedGroupUpdateMessage)
internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
private let kind: Kind
// MARK: Settings
@objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .closedGroupUpdate)) }
@objc internal override func shouldBeSaved() -> Bool { return false }
@objc internal override func shouldSyncTranscript() -> Bool { return false }
// MARK: Kind
internal enum Kind {
case new(groupPublicKey: Data, name: String, groupPrivateKey: Data, senderKeys: [ClosedGroupSenderKey], members: [Data], admins: [Data])
case info(groupPublicKey: Data, name: String, senderKeys: [ClosedGroupSenderKey], members: [Data], admins: [Data])
case senderKeyRequest(groupPublicKey: Data)
case senderKey(groupPublicKey: Data, senderKey: ClosedGroupSenderKey)
}
// MARK: Initialization
internal init(thread: TSThread, kind: Kind) {
self.kind = kind
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "",
attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false,
groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil)
}
required init(dictionary: [String:Any]) throws {
preconditionFailure("Use init(thread:kind:) instead.")
}
// MARK: Coding
internal required init?(coder: NSCoder) {
guard let thread = coder.decodeObject(forKey: "thread") as? TSThread,
let timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64,
let groupPublicKey = coder.decodeObject(forKey: "groupPublicKey") as? Data,
let rawKind = coder.decodeObject(forKey: "kind") as? String else { return nil }
switch rawKind {
case "new":
guard let name = coder.decodeObject(forKey: "name") as? String,
let groupPrivateKey = coder.decodeObject(forKey: "groupPrivateKey") as? Data,
let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey],
let members = coder.decodeObject(forKey: "members") as? [Data],
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
self.kind = .new(groupPublicKey: groupPublicKey, name: name, groupPrivateKey: groupPrivateKey, senderKeys: senderKeys, members: members, admins: admins)
case "info":
guard let name = coder.decodeObject(forKey: "name") as? String,
let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey],
let members = coder.decodeObject(forKey: "members") as? [Data],
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
self.kind = .info(groupPublicKey: groupPublicKey, name: name, senderKeys: senderKeys, members: members, admins: admins)
case "senderKeyRequest":
self.kind = .senderKeyRequest(groupPublicKey: groupPublicKey)
case "senderKey":
guard let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey],
let senderKey = senderKeys.first else { return nil }
self.kind = .senderKey(groupPublicKey: groupPublicKey, senderKey: senderKey)
default: return nil
}
super.init(outgoingMessageWithTimestamp: timestamp, in: thread, messageBody: "",
attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false,
groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil)
}
internal override func encode(with coder: NSCoder) {
coder.encode(thread, forKey: "thread")
coder.encode(timestamp, forKey: "timestamp")
switch kind {
case .new(let groupPublicKey, let name, let groupPrivateKey, let senderKeys, let members, let admins):
coder.encode("new", forKey: "kind")
coder.encode(groupPublicKey, forKey: "groupPublicKey")
coder.encode(name, forKey: "name")
coder.encode(groupPrivateKey, forKey: "groupPrivateKey")
coder.encode(senderKeys, forKey: "senderKeys")
coder.encode(members, forKey: "members")
coder.encode(admins, forKey: "admins")
case .info(let groupPublicKey, let name, let senderKeys, let members, let admins):
coder.encode("info", forKey: "kind")
coder.encode(groupPublicKey, forKey: "groupPublicKey")
coder.encode(name, forKey: "name")
coder.encode(senderKeys, forKey: "senderKeys")
coder.encode(members, forKey: "members")
coder.encode(admins, forKey: "admins")
case .senderKeyRequest(let groupPublicKey):
coder.encode(groupPublicKey, forKey: "groupPublicKey")
case .senderKey(let groupPublicKey, let senderKey):
coder.encode("senderKey", forKey: "kind")
coder.encode(groupPublicKey, forKey: "groupPublicKey")
coder.encode([ senderKey ], forKey: "senderKeys")
}
}
// MARK: Building
@objc internal override func dataMessageBuilder() -> Any? {
guard let builder = super.dataMessageBuilder() as? SSKProtoDataMessage.SSKProtoDataMessageBuilder else { return nil }
do {
let closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate.SSKProtoDataMessageClosedGroupUpdateBuilder
switch kind {
case .new(let groupPublicKey, let name, let groupPrivateKey, let senderKeys, let members, let admins):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .new)
closedGroupUpdate.setName(name)
closedGroupUpdate.setGroupPrivateKey(groupPrivateKey)
closedGroupUpdate.setSenderKeys(try senderKeys.map { try $0.toProto() })
closedGroupUpdate.setMembers(members)
closedGroupUpdate.setAdmins(admins)
case .info(let groupPublicKey, let name, let senderKeys, let members, let admins):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .info)
closedGroupUpdate.setName(name)
closedGroupUpdate.setSenderKeys(try senderKeys.map { try $0.toProto() })
closedGroupUpdate.setMembers(members)
closedGroupUpdate.setAdmins(admins)
case .senderKeyRequest(let groupPublicKey):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .senderKeyRequest)
case .senderKey(let groupPublicKey, let senderKey):
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .senderKey)
closedGroupUpdate.setSenderKeys([ try senderKey.toProto() ])
}
builder.setClosedGroupUpdate(try closedGroupUpdate.build())
} catch {
owsFailDebug("Failed to build closed group update due to error: \(error).")
return nil
}
return builder
}
}

View File

@ -1,70 +0,0 @@
import CryptoSwift
import SessionMetadataKit
@objc(LKClosedGroupUtilities)
public final class ClosedGroupUtilities : NSObject {
@objc(LKSSKDecryptionError)
public class SSKDecryptionError : NSError { // Not called `Error` for Obj-C interoperablity
@objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ])
@objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ])
@objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ])
@objc public static let selfSend = SSKDecryptionError(domain: "SSKErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Message addressed at self." ])
}
@objc(encryptData:usingGroupPublicKey:transaction:error:)
public static func encrypt(data: Data, groupPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Data {
// 1. ) Encrypt the data with the user's sender key
guard let userPublicKey = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else {
throw SMKError.assertionError(description: "[Loki] Couldn't find user key pair.")
}
let ciphertextAndKeyIndex = try SharedSenderKeysImplementation.shared.encrypt(data, forGroupWithPublicKey: groupPublicKey,
senderPublicKey: userPublicKey, protocolContext: transaction)
let ivAndCiphertext = ciphertextAndKeyIndex[0] as! Data
let keyIndex = ciphertextAndKeyIndex[1] as! UInt
let encryptedMessage = ClosedGroupCiphertextMessage(_throws_withIVAndCiphertext: ivAndCiphertext, senderPublicKey: Data(hex: userPublicKey), keyIndex: UInt32(keyIndex))
// 2. ) Encrypt the result for the group's public key to hide the sender public key and key index
let (ciphertext, _, ephemeralPublicKey) = try EncryptionUtilities.encrypt(encryptedMessage.serialized, using: groupPublicKey.removing05PrefixIfNeeded())
// 3. ) Wrap the result
return try SSKProtoClosedGroupCiphertextMessageWrapper.builder(ciphertext: ciphertext, ephemeralPublicKey: ephemeralPublicKey).build().serializedData()
}
@objc(decryptEnvelope:transaction:error:)
public static func decrypt(envelope: SSKProtoEnvelope, transaction: YapDatabaseReadWriteTransaction) throws -> [Any] {
let (plaintext, senderPublicKey) = try decrypt(envelope: envelope, transaction: transaction)
return [ plaintext, senderPublicKey ]
}
public static func decrypt(envelope: SSKProtoEnvelope, transaction: YapDatabaseReadWriteTransaction) throws -> (plaintext: Data, senderPublicKey: String) {
// 1. ) Check preconditions
guard let groupPublicKey = envelope.source, SharedSenderKeysImplementation.shared.isClosedGroup(groupPublicKey) else {
throw SSKDecryptionError.invalidGroupPublicKey
}
guard let data = envelope.content else {
throw SSKDecryptionError.noData
}
guard let hexEncodedGroupPrivateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey) else {
throw SSKDecryptionError.noGroupPrivateKey
}
let groupPrivateKey = Data(hex: hexEncodedGroupPrivateKey)
// 2. ) Parse the wrapper
let wrapper = try SSKProtoClosedGroupCiphertextMessageWrapper.parseData(data)
let ivAndCiphertext = wrapper.ciphertext
let ephemeralPublicKey = wrapper.ephemeralPublicKey
// 3. ) Decrypt the data inside
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: ephemeralPublicKey, privateKey: groupPrivateKey)
let salt = "LOKI"
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
let closedGroupCiphertextMessageAsData = try DecryptionUtilities.decrypt(ivAndCiphertext, usingAESGCMWithSymmetricKey: Data(symmetricKey))
// 4. ) Parse the closed group ciphertext message
let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData)
let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString()
guard senderPublicKey != getUserHexEncodedPublicKey() else { throw SSKDecryptionError.selfSend }
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey,
senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction)
// 6. ) Return
return (plaintext, senderPublicKey)
}
}

View File

@ -1,220 +0,0 @@
import CryptoSwift
import PromiseKit
import SessionMetadataKit
@objc(LKSharedSenderKeysImplementation)
public final class SharedSenderKeysImplementation : NSObject {
private static let gcmTagSize: UInt = 16
private static let ivSize: UInt = 12
// MARK: Documentation
// A quick overview of how shared sender key based closed groups work:
//
// When a user creates a group, they generate a key pair for the group along with a ratchet for
// every member of the group. They bundle this together with some other group info such as the group
// name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of
// the group. Note that because a user can only pick from their existing contacts when selecting
// the group members they shouldn't need to establish sessions before being able to send the
// `ClosedGroupUpdateMessage`. Another way to optimize the performance of the group creation process
// is to batch fetch the device links of all members involved ahead of time, rather than letting
// the sending pipeline do it separately for every user the `ClosedGroupUpdateMessage` is sent to.
// After the group is created, every user polls for the public key associated with the group.
// Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all
// other members of the group they don't yet have a session with for reasons outlined below.
// When a user sends a message they step their ratchet and use the resulting message key to encrypt
// the message.
// When another user receives that message, they step the ratchet associated with the sender and
// use the resulting message key to decrypt the message.
// When a user leaves or is kicked from a group, all members must generate new ratchets to ensure that
// removed users can't decrypt messages going forward. To this end every user deletes all ratchets
// associated with the group in question upon receiving a group update message that indicates that
// a user left. They then generate a new ratchet for themselves and send it out to all members of
// the group (again fetching device links ahead of time). The user should already have established
// sessions with all other members at this point because of the behavior outlined a few points above.
// When a user adds a new member to the group, they generate a ratchet for that new member and
// send that bundled in a `ClosedGroupUpdateMessage` to the group. They send a
// `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of
// every other member of the group to the user that joined.
// MARK: Ratcheting Error
public enum RatchetingError : LocalizedError {
case loadingFailed(groupPublicKey: String, senderPublicKey: String)
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
case generic
public var errorDescription: String? {
switch self {
case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
case .generic: return "An error occurred"
}
}
}
// MARK: Initialization
@objc public static let shared = SharedSenderKeysImplementation()
private override init() { }
// MARK: Private/Internal API
internal func generateRatchet(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> ClosedGroupRatchet {
let rootChainKey = Data.getSecureRandomData(ofSize: 32)!.toHexString()
let ratchet = ClosedGroupRatchet(chainKey: rootChainKey, keyIndex: 0, messageKeys: [])
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction)
return ratchet
}
private func step(_ ratchet: ClosedGroupRatchet) throws -> ClosedGroupRatchet {
let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ])
let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ])
let nextKeyIndex = ratchet.keyIndex + 1
let messageKeys = ratchet.messageKeys + [ nextMessageKey.toHexString() ]
return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: messageKeys)
}
/// - Note: Sync. Don't call from the main thread.
private func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
do {
let result = try step(ratchet)
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
return result
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error
}
}
/// - Note: Sync. Don't call from the main thread.
private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> ClosedGroupRatchet {
#if DEBUG
assert(!Thread.isMainThread)
#endif
let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else {
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
if targetKeyIndex < ratchet.keyIndex {
// There's no need to advance the ratchet if this is invoked for an old key index
guard ratchet.messageKeys.count > targetKeyIndex else {
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
print("[Loki] \(error.errorDescription!)")
throw error
}
return ratchet
} else {
var currentKeyIndex = ratchet.keyIndex
var result = ratchet
while currentKeyIndex < targetKeyIndex {
do {
result = try step(result)
currentKeyIndex = result.keyIndex
} catch {
print("[Loki] Couldn't step ratchet due to error: \(error).")
throw error
}
}
let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction)
return result
}
}
// MARK: Public API
@objc(encrypt:forGroupWithPublicKey:senderPublicKey:protocolContext:error:)
public func encrypt(_ plaintext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, protocolContext: Any) throws -> [Any] {
let transaction = protocolContext as! YapDatabaseReadWriteTransaction
let (ivAndCiphertext, keyIndex) = try encrypt(plaintext, for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
return [ ivAndCiphertext, NSNumber(value: keyIndex) ]
}
public func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> (ivAndCiphertext: Data, keyIndex: UInt) {
let ratchet: ClosedGroupRatchet
do {
ratchet = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
} catch {
// FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more
// convenient because there's an easy way to get the sender public key from here.
if case RatchetingError.loadingFailed(_, _) = error {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
}
throw error
}
let iv = Data.getSecureRandomData(ofSize: SharedSenderKeysImplementation.ivSize)!
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
let messageKey = ratchet.messageKeys.last!
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)
return (ivAndCiphertext: iv + Data(bytes: ciphertext), ratchet.keyIndex)
}
@objc(decrypt:forGroupWithPublicKey:senderPublicKey:keyIndex:protocolContext:error:)
public func decrypt(_ ivAndCiphertext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, protocolContext: Any) throws -> Data {
let transaction = protocolContext as! YapDatabaseReadWriteTransaction
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction)
}
public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> Data {
let ratchet: ClosedGroupRatchet
do {
ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction, isRetry: isRetry)
} catch {
if !isRetry {
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true)
} else {
// FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more
// convenient because there's an easy way to get the sender public key from here.
if case RatchetingError.loadingFailed(_, _) = error {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
}
throw error
}
}
let iv = ivAndCiphertext[0..<Int(SharedSenderKeysImplementation.ivSize)]
let ciphertext = ivAndCiphertext[Int(SharedSenderKeysImplementation.ivSize)...]
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
let messageKeys = ratchet.messageKeys
let lastNMessageKeys: [String]
if messageKeys.count > 16 { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order
lastNMessageKeys = [String](messageKeys[messageKeys.index(messageKeys.endIndex, offsetBy: -16)..<messageKeys.endIndex])
} else {
lastNMessageKeys = messageKeys
}
guard !lastNMessageKeys.isEmpty else {
throw RatchetingError.messageKeyMissing(targetKeyIndex: keyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
}
var error: Error?
for messageKey in lastNMessageKeys.reversed() { // Reversed because most likely the last one is the one we need
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
do {
return Data(try aes.decrypt(ciphertext.bytes))
} catch (let e) {
error = e
}
}
if !isRetry {
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true)
} else {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
throw error ?? RatchetingError.generic
}
}
@objc public func isClosedGroup(_ publicKey: String) -> Bool {
return Storage.getUserClosedGroupPublicKeys().contains(publicKey)
}
public func getKeyPair(forGroupWithPublicKey groupPublicKey: String) -> ECKeyPair {
let privateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey)!
return ECKeyPair(publicKey: Data(hex: groupPublicKey.removing05PrefixIfNeeded()), privateKey: Data(hex: privateKey))!
}
}