mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Implement Session protocol
This commit is contained in:
parent
4cb17b388d
commit
2a4977d269
12 changed files with 106 additions and 18 deletions
1
Podfile
1
Podfile
|
@ -71,6 +71,7 @@ target 'SessionMessagingKit' do
|
|||
pod 'Reachability', :inhibit_warnings => true
|
||||
pod 'SAMKeychain', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'Sodium', :inhibit_warnings => true
|
||||
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
|
|
@ -230,6 +230,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 7699c2a380fc803ef7f51157f1f75da756aa3b45
|
||||
PODFILE CHECKSUM: 3263ab95f60e220882ca53cca4c6bdc2e7a80381
|
||||
|
||||
COCOAPODS: 1.10.0.rc.1
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import PromiseKit
|
||||
import Sodium
|
||||
|
||||
extension Storage {
|
||||
|
||||
|
@ -23,6 +24,16 @@ extension Storage {
|
|||
public func getUserKeyPair() -> ECKeyPair? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()
|
||||
}
|
||||
|
||||
public func getUserED25519KeyPair() -> Box.KeyPair? {
|
||||
let dbConnection = OWSIdentityManager.shared().dbConnection
|
||||
let collection = OWSPrimaryStorageIdentityKeyStoreCollection
|
||||
guard let hexEncodedPublicKey = dbConnection.object(forKey: LKED25519PublicKey, inCollection: collection) as? String,
|
||||
let hexEncodedSecretKey = dbConnection.object(forKey: LKED25519SecretKey, inCollection: collection) as? String else { return nil }
|
||||
let publicKey = Box.KeyPair.PublicKey(hex: hexEncodedPublicKey)
|
||||
let secretKey = Box.KeyPair.SecretKey(hex: hexEncodedSecretKey)
|
||||
return Box.KeyPair(publicKey: publicKey, secretKey: secretKey)
|
||||
}
|
||||
|
||||
public func getUserDisplayName() -> String? {
|
||||
return SSKEnvironment.shared.profileManager.localProfileName()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import CryptoSwift
|
||||
import SessionProtocolKit
|
||||
import SessionUtilitiesKit
|
||||
import Sodium
|
||||
|
||||
internal extension MessageReceiver {
|
||||
|
||||
|
@ -8,7 +9,7 @@ internal extension MessageReceiver {
|
|||
let storage = SNMessagingKitConfiguration.shared.signalStorage
|
||||
let certificateValidator = SNMessagingKitConfiguration.shared.certificateValidator
|
||||
guard let data = envelope.content else { throw Error.noData }
|
||||
guard let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey() else { throw Error.noUserPublicKey }
|
||||
guard let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey() else { throw Error.noUserX25519KeyPair }
|
||||
let cipher = try SMKSecretSessionCipher(sessionResetImplementation: SNMessagingKitConfiguration.shared.sessionRestorationImplementation,
|
||||
sessionStore: storage, preKeyStore: storage, signedPreKeyStore: storage, identityStore: SNMessagingKitConfiguration.shared.identityKeyStore)
|
||||
let result = try cipher.throwswrapped_decryptMessage(certificateValidator: certificateValidator, cipherTextData: data,
|
||||
|
@ -16,6 +17,42 @@ internal extension MessageReceiver {
|
|||
return (result.paddedPayload, result.senderRecipientId)
|
||||
}
|
||||
|
||||
static func decryptWithSessionProtocol(envelope: SNProtoEnvelope) throws -> (plaintext: Data, senderX25519PublicKey: String) {
|
||||
guard let ciphertext = envelope.content else { throw Error.noData }
|
||||
let recipientX25519PrivateKey: Data
|
||||
let recipientX25519PublicKey: Data
|
||||
switch envelope.type {
|
||||
case .unidentifiedSender:
|
||||
guard let userX25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { throw Error.noUserX25519KeyPair }
|
||||
recipientX25519PrivateKey = userX25519KeyPair.privateKey
|
||||
recipientX25519PublicKey = Data(hex: userX25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
|
||||
case .closedGroupCiphertext:
|
||||
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else { throw Error.invalidGroupPublicKey }
|
||||
guard let hexEncodedGroupPrivateKey = SNMessagingKitConfiguration.shared.storage.getClosedGroupPrivateKey(for: hexEncodedGroupPublicKey) else { throw Error.noGroupPrivateKey }
|
||||
recipientX25519PrivateKey = Data(hex: hexEncodedGroupPrivateKey)
|
||||
recipientX25519PublicKey = Data(hex: hexEncodedGroupPublicKey.removing05PrefixIfNeeded())
|
||||
default: preconditionFailure()
|
||||
}
|
||||
let sodium = Sodium()
|
||||
let signatureSize = sodium.sign.Bytes
|
||||
let ed25519PublicKeySize = sodium.sign.PublicKeyBytes
|
||||
|
||||
// 1. ) Decrypt the message
|
||||
guard let plaintextWithMetadata = sodium.box.open(anonymousCipherText: Bytes(ciphertext), recipientPublicKey: Box.PublicKey(Bytes(recipientX25519PublicKey)),
|
||||
recipientSecretKey: Bytes(recipientX25519PrivateKey)), plaintextWithMetadata.count > (signatureSize + ed25519PublicKeySize) else { throw Error.decryptionFailed }
|
||||
// 2. ) Get the message parts
|
||||
let signature = Bytes(plaintextWithMetadata[plaintextWithMetadata.count - signatureSize ..< plaintextWithMetadata.count])
|
||||
let senderED25519PublicKey = Bytes(plaintextWithMetadata[plaintextWithMetadata.count - (signatureSize + ed25519PublicKeySize) ..< plaintextWithMetadata.count - signatureSize])
|
||||
let plaintext = Bytes(plaintextWithMetadata[0..<plaintextWithMetadata.count - (signatureSize + ed25519PublicKeySize)])
|
||||
// 3. ) Verify the signature
|
||||
let isValid = sodium.sign.verify(message: plaintext + senderED25519PublicKey + recipientX25519PublicKey, publicKey: senderED25519PublicKey, signature: signature)
|
||||
guard isValid else { throw Error.invalidSignature }
|
||||
// 4. ) Get the sender's X25519 public key
|
||||
guard let senderX25519PublicKey = sodium.sign.toX25519(ed25519PublicKey: senderED25519PublicKey) else { throw Error.decryptionFailed }
|
||||
|
||||
return (Data(plaintext), "05" + senderX25519PublicKey.toHexString())
|
||||
}
|
||||
|
||||
static func decryptWithSharedSenderKeys(envelope: SNProtoEnvelope, using transaction: Any) throws -> (plaintext: Data, senderPublicKey: String) {
|
||||
// 1. ) Check preconditions
|
||||
guard let groupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(groupPublicKey) else {
|
||||
|
@ -36,8 +73,8 @@ internal extension MessageReceiver {
|
|||
guard let ephemeralSharedSecret = try? Curve25519.generateSharedSecret(fromPublicKey: ephemeralPublicKey, privateKey: groupPrivateKey) else {
|
||||
throw Error.sharedSecretGenerationFailed
|
||||
}
|
||||
let salt = "LOKI"
|
||||
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
|
||||
let salt = "LOKI".data(using: String.Encoding.utf8, allowLossyConversion: true)!.bytes
|
||||
let symmetricKey = try HMAC(key: salt, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
|
||||
let closedGroupCiphertextMessageAsData = try AESGCM.decrypt(ivAndCiphertext, with: Data(symmetricKey))
|
||||
// 4. ) Parse the closed group ciphertext message
|
||||
let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData)
|
||||
|
|
|
@ -7,11 +7,14 @@ public enum MessageReceiver {
|
|||
case invalidMessage
|
||||
case unknownMessage
|
||||
case unknownEnvelopeType
|
||||
case noUserPublicKey
|
||||
case noUserX25519KeyPair
|
||||
case noUserED25519KeyPair
|
||||
case invalidSignature
|
||||
case noData
|
||||
case senderBlocked
|
||||
case noThread
|
||||
case selfSend
|
||||
case decryptionFailed
|
||||
// Shared sender keys
|
||||
case invalidGroupPublicKey
|
||||
case noGroupPrivateKey
|
||||
|
@ -19,7 +22,7 @@ public enum MessageReceiver {
|
|||
|
||||
public var isRetryable: Bool {
|
||||
switch self {
|
||||
case .duplicateMessage, .invalidMessage, .unknownMessage, .unknownEnvelopeType, .noData, .senderBlocked, .selfSend: return false
|
||||
case .duplicateMessage, .invalidMessage, .unknownMessage, .unknownEnvelopeType, .invalidSignature, .noData, .senderBlocked, .selfSend, .decryptionFailed: return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
@ -30,15 +33,18 @@ public enum MessageReceiver {
|
|||
case .invalidMessage: return "Invalid message."
|
||||
case .unknownMessage: return "Unknown message type."
|
||||
case .unknownEnvelopeType: return "Unknown envelope type."
|
||||
case .noUserPublicKey: return "Couldn't find user key pair."
|
||||
case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair."
|
||||
case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair."
|
||||
case .invalidSignature: return "Invalid message signature."
|
||||
case .noData: return "Received an empty envelope."
|
||||
case .senderBlocked: return "Received a message from a blocked user."
|
||||
case .noThread: return "Couldn't find thread for message."
|
||||
case .selfSend: return "Message addressed at self."
|
||||
case .decryptionFailed: return "Decryption failed."
|
||||
// Shared sender keys
|
||||
case .invalidGroupPublicKey: return "Invalid group public key."
|
||||
case .noGroupPrivateKey: return "Missing group private key."
|
||||
case .sharedSecretGenerationFailed: return "Couldn't generate a shared secret."
|
||||
case .selfSend: return "Message addressed at self."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,9 +65,20 @@ public enum MessageReceiver {
|
|||
(plaintext, sender) = (envelope.content!, envelope.source!)
|
||||
} else {
|
||||
switch envelope.type {
|
||||
case .unidentifiedSender: (plaintext, sender) = try decryptWithSignalProtocol(envelope: envelope, using: transaction)
|
||||
case .unidentifiedSender:
|
||||
do {
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(envelope: envelope)
|
||||
} catch {
|
||||
// Migration
|
||||
(plaintext, sender) = try decryptWithSignalProtocol(envelope: envelope, using: transaction)
|
||||
}
|
||||
case .closedGroupCiphertext:
|
||||
(plaintext, sender) = try decryptWithSharedSenderKeys(envelope: envelope, using: transaction)
|
||||
do {
|
||||
(plaintext, sender) = try decryptWithSessionProtocol(envelope: envelope)
|
||||
} catch {
|
||||
// Migration
|
||||
(plaintext, sender) = try decryptWithSharedSenderKeys(envelope: envelope, using: transaction)
|
||||
}
|
||||
groupPublicKey = envelope.source
|
||||
default: throw Error.unknownEnvelopeType
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import SessionProtocolKit
|
||||
import SessionUtilitiesKit
|
||||
import Sodium
|
||||
|
||||
internal extension MessageSender {
|
||||
|
||||
|
@ -11,12 +12,25 @@ internal extension MessageSender {
|
|||
return try cipher.throwswrapped_encryptMessage(recipientPublicKey: publicKey, deviceID: 1, paddedPlaintext: (plaintext as NSData).paddedMessageBody(),
|
||||
senderCertificate: certificate, protocolContext: transaction, useFallbackSessionCipher: true)
|
||||
}
|
||||
|
||||
static func encryptWithSessionProtocol(_ plaintext: Data, for recipientHexEncodedX25519PublicKey: String) throws -> Data {
|
||||
guard let userED25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else { throw Error.noUserED25519KeyPair }
|
||||
let recipientX25519PublicKey = Data(hex: recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
|
||||
let sodium = Sodium()
|
||||
|
||||
let data = plaintext + Data(userED25519KeyPair.publicKey) + recipientX25519PublicKey
|
||||
guard let signature = sodium.sign.signature(message: Bytes(data), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed }
|
||||
guard let ciphertext = sodium.box.seal(message: Bytes(plaintext + Data(userED25519KeyPair.publicKey)
|
||||
+ Data(signature)), recipientPublicKey: Bytes(recipientX25519PublicKey)) else { throw Error.encryptionFailed }
|
||||
|
||||
return Data(ciphertext)
|
||||
}
|
||||
|
||||
static func encryptWithSharedSenderKeys(_ plaintext: Data, for groupPublicKey: String, using transaction: Any) throws -> Data {
|
||||
// 1. ) Encrypt the data with the user's sender key
|
||||
guard let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey() else {
|
||||
SNLog("Couldn't find user key pair.")
|
||||
throw Error.noUserPublicKey
|
||||
throw Error.noUserX25519KeyPair
|
||||
}
|
||||
let (ivAndCiphertext, keyIndex) = try SharedSenderKeys.encrypt((plaintext as NSData).paddedMessageBody(), for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
|
||||
let encryptedMessage = ClosedGroupCiphertextMessage(_throws_withIVAndCiphertext: ivAndCiphertext, senderPublicKey: Data(hex: userPublicKey), keyIndex: UInt32(keyIndex))
|
||||
|
|
|
@ -10,7 +10,10 @@ public final class MessageSender : NSObject {
|
|||
case invalidMessage
|
||||
case protoConversionFailed
|
||||
case proofOfWorkCalculationFailed
|
||||
case noUserPublicKey
|
||||
case noUserX25519KeyPair
|
||||
case noUserED25519KeyPair
|
||||
case signingFailed
|
||||
case encryptionFailed
|
||||
// Closed groups
|
||||
case noThread
|
||||
case noPrivateKey
|
||||
|
@ -18,7 +21,7 @@ public final class MessageSender : NSObject {
|
|||
|
||||
internal var isRetryable: Bool {
|
||||
switch self {
|
||||
case .invalidMessage, .protoConversionFailed, .proofOfWorkCalculationFailed, .invalidClosedGroupUpdate: return false
|
||||
case .invalidMessage, .protoConversionFailed, .proofOfWorkCalculationFailed, .invalidClosedGroupUpdate, .signingFailed, .encryptionFailed: return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +31,10 @@ public final class MessageSender : NSObject {
|
|||
case .invalidMessage: return "Invalid message."
|
||||
case .protoConversionFailed: return "Couldn't convert message to proto."
|
||||
case .proofOfWorkCalculationFailed: return "Proof of work calculation failed."
|
||||
case .noUserPublicKey: return "Couldn't find user key pair."
|
||||
case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair."
|
||||
case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair."
|
||||
case .signingFailed: return "Couldn't sign message."
|
||||
case .encryptionFailed: return "Couldn't encrypt message."
|
||||
// Closed groups
|
||||
case .noThread: return "Couldn't find a thread associated with the given group public key."
|
||||
case .noPrivateKey: return "Couldn't find a private key associated with the given group public key."
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import SessionProtocolKit
|
||||
import PromiseKit
|
||||
import Sodium
|
||||
|
||||
public protocol SessionMessagingKitStorageProtocol {
|
||||
|
||||
|
@ -15,6 +16,7 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
|
||||
func getUserPublicKey() -> String?
|
||||
func getUserKeyPair() -> ECKeyPair?
|
||||
func getUserED25519KeyPair() -> Box.KeyPair?
|
||||
func getUserDisplayName() -> String?
|
||||
func getUserProfileKey() -> Data?
|
||||
func getUserProfilePictureURL() -> String?
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
var digitsOnly: String {
|
||||
return (self as NSString).digitsOnly()
|
||||
}
|
||||
|
|
|
@ -500,7 +500,6 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
|
|||
|
||||
- (UIView *)addBorderViewWithColor:(UIColor *)color strokeWidth:(CGFloat)strokeWidth cornerRadius:(CGFloat)cornerRadius
|
||||
{
|
||||
|
||||
UIView *borderView = [UIView new];
|
||||
borderView.userInteractionEnabled = NO;
|
||||
borderView.backgroundColor = UIColor.clearColor;
|
||||
|
|
|
@ -244,6 +244,7 @@
|
|||
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8566C62256F55930045A0B9 /* OWSLinkPreview+Conversion.swift */; };
|
||||
B8566C6C256F60F50045A0B9 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2D1255B6DAF007E1867 /* OWSUserProfile.m */; };
|
||||
B8566C7D256F62030045A0B9 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2D3255B6DAF007E1867 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B866CE112581C1A900535CC4 /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; };
|
||||
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
|
||||
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
|
||||
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */; };
|
||||
|
@ -914,7 +915,6 @@
|
|||
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
||||
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */; };
|
||||
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */; };
|
||||
C3E7134F251C867C009649BB /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; };
|
||||
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ECBF7A257056B700EA7FCE /* Threading.swift */; };
|
||||
C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */; };
|
||||
D2179CFC16BB0B3A0006F3AB /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */; };
|
||||
|
@ -2603,7 +2603,6 @@
|
|||
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */,
|
||||
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
|
||||
B886B4A82398BA1500211ABE /* QRCode.swift */,
|
||||
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
|
||||
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */,
|
||||
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
|
||||
|
@ -3372,6 +3371,7 @@
|
|||
C33FDB91255A581200E217F9 /* ProtoUtils.h */,
|
||||
C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */,
|
||||
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
|
||||
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
|
||||
C33FDB31255A580A00E217F9 /* SSKEnvironment.h */,
|
||||
C33FDAF4255A580600E217F9 /* SSKEnvironment.m */,
|
||||
C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */,
|
||||
|
@ -5253,6 +5253,7 @@
|
|||
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
|
||||
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
|
||||
C32C5AAB256DBE8F003C73A2 /* TSIncomingMessage+Conversion.swift in Sources */,
|
||||
B866CE112581C1A900535CC4 /* Sodium+Conversion.swift in Sources */,
|
||||
C32C5A88256DBCF9003C73A2 /* MessageReceiver+Handling.swift in Sources */,
|
||||
C32C5C1B256DC9E0003C73A2 /* General.swift in Sources */,
|
||||
C32C5A02256DB658003C73A2 /* MessageSender+Convenience.swift in Sources */,
|
||||
|
@ -5522,7 +5523,6 @@
|
|||
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */,
|
||||
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */,
|
||||
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */,
|
||||
C3E7134F251C867C009649BB /* Sodium+Conversion.swift in Sources */,
|
||||
340FC8B5204DAC8D007AEB0F /* AboutTableViewController.m in Sources */,
|
||||
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
|
|
Loading…
Reference in a new issue