Re-organize files
This commit is contained in:
parent
41d078d012
commit
a3382f41d4
2
Podfile
2
Podfile
|
@ -121,7 +121,7 @@ target 'SessionSnodeKit' do
|
||||||
pod 'PromiseKit', :inhibit_warnings => true
|
pod 'PromiseKit', :inhibit_warnings => true
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'SessionUtilities' do
|
target 'SessionUtilitiesKit' do
|
||||||
pod 'CryptoSwift', :inhibit_warnings => true
|
pod 'CryptoSwift', :inhibit_warnings => true
|
||||||
pod 'Curve25519Kit', :inhibit_warnings => true
|
pod 'Curve25519Kit', :inhibit_warnings => true
|
||||||
pod 'PromiseKit', :inhibit_warnings => true
|
pod 'PromiseKit', :inhibit_warnings => true
|
||||||
|
|
|
@ -332,6 +332,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||||
|
|
||||||
PODFILE CHECKSUM: 2efffb9efc5ed9feaf8b2df90d8dee2d4685e7c2
|
PODFILE CHECKSUM: 278b25019daa575575de0bf9baf371f7cdcd4fc4
|
||||||
|
|
||||||
COCOAPODS: 1.10.0.rc.1
|
COCOAPODS: 1.10.0.rc.1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
// TODO: Implementation
|
// TODO: Implementation
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
// TODO: Implementation
|
// TODO: Implementation
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class JobQueue : JobDelegate {
|
public final class JobQueue : JobDelegate {
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||||
public var delegate: JobDelegate?
|
public var delegate: JobDelegate?
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||||
public var delegate: JobDelegate?
|
public var delegate: JobDelegate?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||||
public var delegate: JobDelegate?
|
public var delegate: JobDelegate?
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SessionProtocolKit
|
import SessionProtocolKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
@objc(SNClosedGroupUpdate)
|
@objc(SNClosedGroupUpdate)
|
||||||
public final class ClosedGroupUpdate : ControlMessage {
|
public final class ClosedGroupUpdate : ControlMessage {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
@objc(SNExpirationTimerUpdate)
|
@objc(SNExpirationTimerUpdate)
|
||||||
public final class ExpirationTimerUpdate : ControlMessage {
|
public final class ExpirationTimerUpdate : ControlMessage {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
@objc(SNReadReceipt)
|
@objc(SNReadReceipt)
|
||||||
public final class ReadReceipt : ControlMessage {
|
public final class ReadReceipt : ControlMessage {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SessionProtocolKit
|
import SessionProtocolKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
@objc(SNSessionRequest)
|
@objc(SNSessionRequest)
|
||||||
public final class SessionRequest : ControlMessage {
|
public final class SessionRequest : ControlMessage {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
@objc(SNTypingIndicator)
|
@objc(SNTypingIndicator)
|
||||||
public final class TypingIndicator : ControlMessage {
|
public final class TypingIndicator : ControlMessage {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public extension VisibleMessage {
|
public extension VisibleMessage {
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public extension VisibleMessage {
|
public extension VisibleMessage {
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public extension VisibleMessage {
|
public extension VisibleMessage {
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
@objc(SNVisibleMessage)
|
@objc(SNVisibleMessage)
|
||||||
public final class VisibleMessage : Message {
|
public final class VisibleMessage : Message {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CryptoSwift
|
import CryptoSwift
|
||||||
import SessionProtocolKit
|
import SessionProtocolKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
internal extension MessageReceiver {
|
internal extension MessageReceiver {
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
internal enum MessageReceiver {
|
internal enum MessageReceiver {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SessionProtocolKit
|
import SessionProtocolKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
internal extension MessageSender {
|
internal extension MessageSender {
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
internal enum MessageSender {
|
internal enum MessageSender {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public enum MessageWrapper {
|
public enum MessageWrapper {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
enum ProofOfWork {
|
enum ProofOfWork {
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class ClosedGroupRatchet : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
public final class ClosedGroupRatchet : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||||
public let chainKey: String
|
public let chainKey: String
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CryptoSwift
|
import CryptoSwift
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public protocol SharedSenderKeysDelegate {
|
public protocol SharedSenderKeysDelegate {
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CryptoSwift
|
import CryptoSwift
|
||||||
import Curve25519Kit
|
import Curve25519Kit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
/// A fallback session cipher which uses the the recipient's public key to encrypt data.
|
/// A fallback session cipher which uses the the recipient's public key to encrypt data.
|
||||||
@objc public final class FallBackSessionCipher : NSObject {
|
@objc public final class FallBackSessionCipher : NSObject {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CryptoSwift
|
import CryptoSwift
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
internal extension OnionRequestAPI {
|
internal extension OnionRequestAPI {
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import CryptoSwift
|
import CryptoSwift
|
||||||
import PromiseKit
|
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.
|
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||||
public enum OnionRequestAPI {
|
public enum OnionRequestAPI {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public enum SnodeAPI {
|
public enum SnodeAPI {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PromiseKit
|
import PromiseKit
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public final class SnodeMessage : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
public final class SnodeMessage : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||||
/// The hex encoded public key of the recipient.
|
/// The hex encoded public key of the recipient.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SessionUtilities
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public protocol SessionSnodeKitStorageProtocol {
|
public protocol SessionSnodeKitStorageProtocol {
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
|
@ -24,8 +24,7 @@ public enum Mnemonic {
|
||||||
if let cachedResult = Language.wordSetCache[self] {
|
if let cachedResult = Language.wordSetCache[self] {
|
||||||
return cachedResult
|
return cachedResult
|
||||||
} else {
|
} else {
|
||||||
let bundleID = "org.cocoapods.SessionServiceKit"
|
let url = Bundle.main.url(forResource: filename, withExtension: "txt")!
|
||||||
let url = Bundle(identifier: bundleID)!.url(forResource: filename, withExtension: "txt")!
|
|
||||||
let contents = try! String(contentsOf: url)
|
let contents = try! String(contentsOf: url)
|
||||||
let result = contents.split(separator: ",").map { String($0) }
|
let result = contents.split(separator: ",").map { String($0) }
|
||||||
Language.wordSetCache[self] = result
|
Language.wordSetCache[self] = result
|
|
@ -470,7 +470,7 @@
|
||||||
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
||||||
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
|
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
|
||||||
7BF3FF002505B8E400609570 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */; };
|
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 */; };
|
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */; };
|
||||||
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||||
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.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 */; };
|
C3A71D682558A0170043A11F /* LokiSessionCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D652558A0170043A11F /* LokiSessionCipher.swift */; };
|
||||||
C3A71D742558A0F60043A11F /* SMKProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D722558A0F60043A11F /* SMKProto.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 */; };
|
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 */; };
|
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
|
||||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
|
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
|
||||||
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE07F2554CDD70050F1E3 /* Storage.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 */; };
|
C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; };
|
||||||
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.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 */; };
|
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, ); }; };
|
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
C3C2A680255388CC00C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
|
C3C2A680255388CC00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||||
C3C2A681255388CC00C340D1 /* SessionUtilities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
C3C2A6C62553896A00C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
|
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, ); }; };
|
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 */; };
|
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, ); }; };
|
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 */; };
|
C3C2ABF82553C8A300C340D1 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2ABF72553C8A300C340D1 /* Storage.swift */; };
|
||||||
C3C2AC0A2553C9A100C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC092553C9A100C340D1 /* Configuration.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 */; };
|
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 */; };
|
C3C3CF8924D8EED300E1CCE7 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C3CF8824D8EED300E1CCE7 /* TextView.swift */; };
|
||||||
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
|
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
|
||||||
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
||||||
|
@ -926,7 +927,7 @@
|
||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
C3C2A86A2553B41A00C340D1 /* SessionProtocolKit.framework in Embed Frameworks */,
|
C3C2A86A2553B41A00C340D1 /* SessionProtocolKit.framework in Embed Frameworks */,
|
||||||
C3C2A681255388CC00C340D1 /* SessionUtilities.framework in Embed Frameworks */,
|
C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */,
|
||||||
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */,
|
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */,
|
||||||
4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */,
|
4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */,
|
||||||
C3C2A6F825539DE700C340D1 /* SessionMessagingKit.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>"; };
|
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; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C3C2A67B255388CC00C340D1 /* SessionUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilities.h; sourceTree = "<group>"; };
|
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>"; };
|
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; };
|
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>"; };
|
C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionMessagingKit.h; sourceTree = "<group>"; };
|
||||||
|
@ -1901,7 +1905,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */,
|
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */,
|
||||||
C3C2A6C62553896A00C340D1 /* SessionUtilities.framework in Frameworks */,
|
C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1909,7 +1913,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
9C9B845C8451114076E55902 /* Pods_SessionUtilities.framework in Frameworks */,
|
945AA2B82B621254F69FA9E8 /* Pods_SessionUtilitiesKit.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1928,7 +1932,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A33A4BA9D050805FE156E3ED /* Pods_SessionProtocolKit.framework in Frameworks */,
|
A33A4BA9D050805FE156E3ED /* Pods_SessionProtocolKit.framework in Frameworks */,
|
||||||
C3C2AC372553CCE600C340D1 /* SessionUtilities.framework in Frameworks */,
|
C3C2AC372553CCE600C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1959,7 +1963,7 @@
|
||||||
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */,
|
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */,
|
||||||
A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */,
|
A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */,
|
||||||
A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */,
|
A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */,
|
||||||
C3C2A680255388CC00C340D1 /* SessionUtilities.framework in Frameworks */,
|
C3C2A680255388CC00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||||
A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */,
|
A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */,
|
||||||
D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */,
|
D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */,
|
||||||
D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */,
|
D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */,
|
||||||
|
@ -2979,6 +2983,8 @@
|
||||||
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */,
|
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */,
|
||||||
AEA8083C060FF9BAFF6E0C9F /* Pods-SessionProtocolKit.debug.xcconfig */,
|
AEA8083C060FF9BAFF6E0C9F /* Pods-SessionProtocolKit.debug.xcconfig */,
|
||||||
174BD0AE74771D02DAC2B7A9 /* Pods-SessionProtocolKit.app store release.xcconfig */,
|
174BD0AE74771D02DAC2B7A9 /* Pods-SessionProtocolKit.app store release.xcconfig */,
|
||||||
|
264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */,
|
||||||
|
7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
name = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3421,7 +3427,7 @@
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
C3C2A67A255388CC00C340D1 /* SessionUtilities */ = {
|
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C3C2A68B255388D500C340D1 /* Meta */,
|
C3C2A68B255388D500C340D1 /* Meta */,
|
||||||
|
@ -3435,6 +3441,7 @@
|
||||||
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
|
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
|
||||||
C3C2A5D92553860B00C340D1 /* JSON.swift */,
|
C3C2A5D92553860B00C340D1 /* JSON.swift */,
|
||||||
C3C2A5CE2553860700C340D1 /* Logging.swift */,
|
C3C2A5CE2553860700C340D1 /* Logging.swift */,
|
||||||
|
C3A71F882558BA9F0043A11F /* Mnemonic.swift */,
|
||||||
C300A6302554B68200555489 /* NSDate+Timestamp.h */,
|
C300A6302554B68200555489 /* NSDate+Timestamp.h */,
|
||||||
C300A6312554B6D100555489 /* NSDate+Timestamp.mm */,
|
C300A6312554B6D100555489 /* NSDate+Timestamp.mm */,
|
||||||
C352A3762557859C00338F3E /* NSTimer+Proxying.h */,
|
C352A3762557859C00338F3E /* NSTimer+Proxying.h */,
|
||||||
|
@ -3444,13 +3451,13 @@
|
||||||
C352A3A42557B5F000338F3E /* TSRequest.h */,
|
C352A3A42557B5F000338F3E /* TSRequest.h */,
|
||||||
C352A3A52557B60D00338F3E /* TSRequest.m */,
|
C352A3A52557B60D00338F3E /* TSRequest.m */,
|
||||||
);
|
);
|
||||||
path = SessionUtilities;
|
path = SessionUtilitiesKit;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
C3C2A68B255388D500C340D1 /* Meta */ = {
|
C3C2A68B255388D500C340D1 /* Meta */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C3C2A67B255388CC00C340D1 /* SessionUtilities.h */,
|
C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */,
|
||||||
C3C2A67C255388CC00C340D1 /* Info.plist */,
|
C3C2A67C255388CC00C340D1 /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = Meta;
|
path = Meta;
|
||||||
|
@ -3702,7 +3709,7 @@
|
||||||
C3C2A6F125539DE700C340D1 /* SessionMessagingKit */,
|
C3C2A6F125539DE700C340D1 /* SessionMessagingKit */,
|
||||||
C3C2A8632553B41A00C340D1 /* SessionProtocolKit */,
|
C3C2A8632553B41A00C340D1 /* SessionProtocolKit */,
|
||||||
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
|
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
|
||||||
C3C2A67A255388CC00C340D1 /* SessionUtilities */,
|
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */,
|
||||||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||||
D221A08A169C9E5E00537ABF /* Products */,
|
D221A08A169C9E5E00537ABF /* Products */,
|
||||||
9404664EC513585B05DF1350 /* Pods */,
|
9404664EC513585B05DF1350 /* Pods */,
|
||||||
|
@ -3718,7 +3725,7 @@
|
||||||
453518921FC63DBF00210559 /* SignalMessaging.framework */,
|
453518921FC63DBF00210559 /* SignalMessaging.framework */,
|
||||||
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
|
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
|
||||||
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */,
|
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */,
|
||||||
C3C2A679255388CC00C340D1 /* SessionUtilities.framework */,
|
C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */,
|
||||||
C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */,
|
C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */,
|
||||||
C3C2A8622553B41A00C340D1 /* SessionProtocolKit.framework */,
|
C3C2A8622553B41A00C340D1 /* SessionProtocolKit.framework */,
|
||||||
);
|
);
|
||||||
|
@ -3769,9 +3776,9 @@
|
||||||
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
|
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
|
||||||
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
|
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
|
||||||
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */,
|
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */,
|
||||||
686875887229AB29C07145BA /* Pods_SessionUtilities.framework */,
|
|
||||||
FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */,
|
FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */,
|
||||||
2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */,
|
2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */,
|
||||||
|
9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3905,7 +3912,7 @@
|
||||||
C300A63B2554B72200555489 /* NSDate+Timestamp.h in Headers */,
|
C300A63B2554B72200555489 /* NSDate+Timestamp.h in Headers */,
|
||||||
C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */,
|
C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */,
|
||||||
C3471F5625553E1100297E91 /* ECKeyPair+Utilities.h in Headers */,
|
C3471F5625553E1100297E91 /* ECKeyPair+Utilities.h in Headers */,
|
||||||
C3C2A67D255388CC00C340D1 /* SessionUtilities.h in Headers */,
|
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -4051,9 +4058,9 @@
|
||||||
productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */;
|
productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */;
|
||||||
productType = "com.apple.product-type.framework";
|
productType = "com.apple.product-type.framework";
|
||||||
};
|
};
|
||||||
C3C2A678255388CC00C340D1 /* SessionUtilities */ = {
|
C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilities" */;
|
buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
83DABC75697364620557C68B /* [CP] Check Pods Manifest.lock */,
|
83DABC75697364620557C68B /* [CP] Check Pods Manifest.lock */,
|
||||||
C3C2A674255388CC00C340D1 /* Headers */,
|
C3C2A674255388CC00C340D1 /* Headers */,
|
||||||
|
@ -4065,9 +4072,9 @@
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = SessionUtilities;
|
name = SessionUtilitiesKit;
|
||||||
productName = SessionUtilities;
|
productName = SessionUtilities;
|
||||||
productReference = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */;
|
productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */;
|
||||||
productType = "com.apple.product-type.framework";
|
productType = "com.apple.product-type.framework";
|
||||||
};
|
};
|
||||||
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */ = {
|
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */ = {
|
||||||
|
@ -4299,7 +4306,7 @@
|
||||||
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */,
|
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */,
|
||||||
C3C2A8612553B41A00C340D1 /* SessionProtocolKit */,
|
C3C2A8612553B41A00C340D1 /* SessionProtocolKit */,
|
||||||
C3C2A59E255385C100C340D1 /* SessionSnodeKit */,
|
C3C2A59E255385C100C340D1 /* SessionSnodeKit */,
|
||||||
C3C2A678255388CC00C340D1 /* SessionUtilities */,
|
C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
@ -4708,7 +4715,7 @@
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
);
|
);
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
"$(DERIVED_FILE_DIR)/Pods-SessionUtilities-checkManifestLockResult.txt",
|
"$(DERIVED_FILE_DIR)/Pods-SessionUtilitiesKit-checkManifestLockResult.txt",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
|
@ -5017,15 +5024,16 @@
|
||||||
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
|
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
|
||||||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||||
|
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */,
|
||||||
C3471F6825553E7600297E91 /* ECKeyPair+Utilities.m in Sources */,
|
C3471F6825553E7600297E91 /* ECKeyPair+Utilities.m in Sources */,
|
||||||
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
|
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
|
||||||
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
|
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
|
||||||
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
|
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
|
||||||
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
|
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
|
||||||
|
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
|
||||||
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
||||||
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
|
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
|
||||||
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
||||||
C3A71D862558A28A0043A11F /* DiffieHellman.swift in Sources */,
|
|
||||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -5477,7 +5485,7 @@
|
||||||
};
|
};
|
||||||
C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = {
|
C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = C3C2A678255388CC00C340D1 /* SessionUtilities */;
|
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
|
||||||
targetProxy = C3C2A67E255388CC00C340D1 /* PBXContainerItemProxy */;
|
targetProxy = C3C2A67E255388CC00C340D1 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
C3C2A6F625539DE700C340D1 /* PBXTargetDependency */ = {
|
C3C2A6F625539DE700C340D1 /* PBXTargetDependency */ = {
|
||||||
|
@ -6015,7 +6023,7 @@
|
||||||
};
|
};
|
||||||
C3C2A682255388CC00C340D1 /* Debug */ = {
|
C3C2A682255388CC00C340D1 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = E7E2FBF1546840C91B7E4879 /* Pods-SessionUtilities.debug.xcconfig */;
|
baseConfigurationReference = 264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
@ -6047,7 +6055,7 @@
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
INFOPLIST_FILE = SessionUtilities/Meta/Info.plist;
|
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||||
|
@ -6062,7 +6070,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"UIKit\"",
|
"\"UIKit\"",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilities";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
@ -6076,7 +6084,7 @@
|
||||||
};
|
};
|
||||||
C3C2A683255388CC00C340D1 /* App Store Release */ = {
|
C3C2A683255388CC00C340D1 /* App Store Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 3303495F6651CE2F3CC9693B /* Pods-SessionUtilities.app store release.xcconfig */;
|
baseConfigurationReference = 7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
@ -6129,7 +6137,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
INFOPLIST_FILE = SessionUtilities/Meta/Info.plist;
|
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||||
|
@ -6144,7 +6152,7 @@
|
||||||
"-framework",
|
"-framework",
|
||||||
"\"UIKit\"",
|
"\"UIKit\"",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilities";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -6854,7 +6862,7 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = "App Store Release";
|
defaultConfigurationName = "App Store Release";
|
||||||
};
|
};
|
||||||
C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilities" */ = {
|
C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C3C2A682255388CC00C340D1 /* Debug */,
|
C3C2A682255388CC00C340D1 /* Debug */,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)" }
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) ]" }
|
|
||||||
}
|
|
|
@ -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()) ]"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))!
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue