diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8c139ea7d..8d93cf779 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -788,6 +788,15 @@ FD078E6027E2BB36000769AF /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5F27E2BB36000769AF /* MockIdentityManager.swift */; }; FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; }; FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; }; + FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; }; + FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; + FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; }; + FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; }; + FD3C906727E416AF00CD579F /* BlindedIdMappingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */; }; + FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */; }; + FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; }; + FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906E27E43E8700CD579F /* MockBox.swift */; }; + FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; }; FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; }; FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; }; @@ -1938,6 +1947,15 @@ FD078E5F27E2BB36000769AF /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.swift; sourceTree = ""; }; FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = ""; }; FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = ""; }; + FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = ""; }; + FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; + FD3C906127E411AF00CD579F /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = ""; }; + FD3C906327E4122F00CD579F /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = ""; }; + FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMappingSpec.swift; sourceTree = ""; }; + FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumUtilitiesSpec.swift; sourceTree = ""; }; + FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = ""; }; + FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = ""; }; + FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = ""; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; @@ -3884,6 +3902,49 @@ path = Session; sourceTree = ""; }; + FD3C905D27E410DB00CD579F /* Common Networking */ = { + isa = PBXGroup; + children = ( + FD3C905E27E410EE00CD579F /* Models */, + FD3C906127E411AF00CD579F /* HeaderSpec.swift */, + FD3C906327E4122F00CD579F /* RequestSpec.swift */, + ); + path = "Common Networking"; + sourceTree = ""; + }; + FD3C905E27E410EE00CD579F /* Models */ = { + isa = PBXGroup; + children = ( + FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD3C906527E416A200CD579F /* Contacts */ = { + isa = PBXGroup; + children = ( + FD3C906627E416AF00CD579F /* BlindedIdMappingSpec.swift */, + ); + path = Contacts; + sourceTree = ""; + }; + FD3C906827E417B100CD579F /* Utilities */ = { + isa = PBXGroup; + children = ( + FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + FD3C906B27E43C2400CD579F /* Sending & Receiving */ = { + isa = PBXGroup; + children = ( + FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */, + FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */, + ); + path = "Sending & Receiving"; + sourceTree = ""; + }; FD659ABE27A7648200F12C02 /* Message Requests */ = { isa = PBXGroup; children = ( @@ -3924,6 +3985,7 @@ children = ( FD83B9C227CF33F7005E1583 /* ServerSpec.swift */, FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */, + FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */, FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */, FDC2908627D7047F005DAE71 /* RoomSpec.swift */, FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */, @@ -4038,7 +4100,11 @@ isa = PBXGroup; children = ( FDC4389B27BA01E300C60D73 /* _TestUtilities */, + FD3C905D27E410DB00CD579F /* Common Networking */, + FD3C906527E416A200CD579F /* Contacts */, + FD3C906B27E43C2400CD579F /* Sending & Receiving */, FDC4389827BA001800C60D73 /* Open Groups */, + FD3C906827E417B100CD579F /* Utilities */, ); path = SessionMessagingKitTests; sourceTree = ""; @@ -4061,9 +4127,10 @@ FD078E5F27E2BB36000769AF /* MockIdentityManager.swift */, FDC4389C27BA01F000C60D73 /* MockStorage.swift */, FD859EF327C2F49200510D0C /* MockSodium.swift */, + FD3C906E27E43E8700CD579F /* MockBox.swift */, + FD859EF927C2F5C500510D0C /* MockGenericHash.swift */, FD859EF527C2F52C00510D0C /* MockSign.swift */, FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */, - FD859EF927C2F5C500510D0C /* MockGenericHash.swift */, FD859EFB27C2F60700510D0C /* MockEd25519.swift */, FD078E5927E29F09000769AF /* MockNonce16Generator.swift */, FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */, @@ -5673,20 +5740,25 @@ buildActionMask = 2147483647; files = ( FDC290AC27DB0B1C005DAE71 /* MockedExtensions.swift in Sources */, + FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */, FD078E5827E1B831000769AF /* TestIncomingMessage.swift in Sources */, FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */, FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, FDC290AF27DFEE97005DAE71 /* TestTransaction.swift in Sources */, FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */, FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, + FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */, FD078E6027E2BB36000769AF /* MockIdentityManager.swift in Sources */, + FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */, FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, FD078E4F27E175F1000769AF /* DependencyExtensions.swift in Sources */, FDC2909C27D713D2005DAE71 /* SodiumProtocolsSpec.swift in Sources */, + FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */, FD83B9C327CF33F7005E1583 /* ServerSpec.swift in Sources */, FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */, + FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */, FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */, FDC290A027D85826005DAE71 /* TestContactThread.swift in Sources */, FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */, @@ -5697,20 +5769,24 @@ FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */, FDC290A627D860CE005DAE71 /* Mock.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, + FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, + FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */, FDC290B727E00FDB005DAE71 /* TestGroupThread.swift in Sources */, FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */, FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */, FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, FD078E5A27E29F09000769AF /* MockNonce16Generator.swift in Sources */, + FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */, FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, FDC290A227D85890005DAE71 /* TestInteraction.swift in Sources */, FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */, FDC4389D27BA01F000C60D73 /* MockStorage.swift in Sources */, + FD3C906727E416AF00CD579F /* BlindedIdMappingSpec.swift in Sources */, FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */, FD078E5C27E29F78000769AF /* MockNonce24Generator.swift in Sources */, ); diff --git a/SessionMessagingKit/Common Networking/Request.swift b/SessionMessagingKit/Common Networking/Request.swift index 19130eb98..f32620bb2 100644 --- a/SessionMessagingKit/Common Networking/Request.swift +++ b/SessionMessagingKit/Common Networking/Request.swift @@ -96,3 +96,5 @@ struct Request { return urlRequest } } + +extension Request: Equatable where T: Equatable {} diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index f6f10ca10..3a7035910 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -21,12 +21,6 @@ public protocol OGMCacheType { func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval } -extension OGMCacheType { - func getTimeSinceLastOpen() -> TimeInterval { - return getTimeSinceLastOpen(using: Dependencies()) - } -} - // MARK: - OpenGroupManager @objc(SNOpenGroupManager) @@ -49,7 +43,7 @@ public final class OpenGroupManager: NSObject { public var timeSinceLastPoll: [String: TimeInterval] = [:] fileprivate var _timeSinceLastOpen: TimeInterval? - public func getTimeSinceLastOpen(using dependencies: Dependencies = Dependencies()) -> TimeInterval { + public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen { return storedTimeSinceLastOpen } @@ -702,9 +696,10 @@ extension OpenGroupManager { identityManager: IdentityManagerProtocol? = nil, storage: SessionMessagingKitStorageProtocol? = nil, sodium: SodiumType? = nil, - aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, - sign: SignType? = nil, + box: BoxType? = nil, genericHash: GenericHashType? = nil, + sign: SignType? = nil, + aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, ed25519: Ed25519Type? = nil, nonceGenerator16: NonceGenerator16ByteType? = nil, nonceGenerator24: NonceGenerator24ByteType? = nil, @@ -718,9 +713,10 @@ extension OpenGroupManager { identityManager: identityManager, storage: storage, sodium: sodium, - aeadXChaCha20Poly1305Ietf: aeadXChaCha20Poly1305Ietf, - sign: sign, + box: box, genericHash: genericHash, + sign: sign, + aeadXChaCha20Poly1305Ietf: aeadXChaCha20Poly1305Ietf, ed25519: ed25519, nonceGenerator16: nonceGenerator16, nonceGenerator24: nonceGenerator24, diff --git a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift index d37137ee1..a9f15622a 100644 --- a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift +++ b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift @@ -5,18 +5,19 @@ import Sodium import Curve25519Kit public protocol SodiumType { + func getBox() -> BoxType func getGenericHash() -> GenericHashType - func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType func getSign() -> SignType + func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType - func generateBlindingFactor(serverPublicKey: String) -> Bytes? + func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? - func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool + func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool } public protocol AeadXChaCha20Poly1305IetfType { @@ -32,12 +33,9 @@ public protocol Ed25519Type { func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool } -public protocol SignType { - var PublicKeyBytes: Int { get } - - func toX25519(ed25519PublicKey: Bytes) -> Bytes? - func signature(message: Bytes, secretKey: Bytes) -> Bytes? - func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool +public protocol BoxType { + func seal(message: Bytes, recipientPublicKey: Bytes) -> Bytes? + func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Bytes? } public protocol GenericHashType { @@ -46,8 +44,25 @@ public protocol GenericHashType { func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? } +public protocol SignType { + var Bytes: Int { get } + var PublicKeyBytes: Int { get } + + func toX25519(ed25519PublicKey: Bytes) -> Bytes? + func signature(message: Bytes, secretKey: Bytes) -> Bytes? + func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool +} + // MARK: - Default Values +extension GenericHashType { + func hash(message: Bytes) -> Bytes? { return hash(message: message, key: nil) } + + func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? { + return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal) + } +} + extension AeadXChaCha20Poly1305IetfType { func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? { return encrypt(message: message, secretKey: secretKey, nonce: nonce, additionalData: nil) @@ -58,17 +73,10 @@ extension AeadXChaCha20Poly1305IetfType { } } -extension GenericHashType { - func hash(message: Bytes) -> Bytes? { return hash(message: message, key: nil) } - - func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? { - return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal) - } -} - // MARK: - Conformance extension Sodium: SodiumType { + public func getBox() -> BoxType { return box } public func getGenericHash() -> GenericHashType { return genericHash } public func getSign() -> SignType { return sign } public func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return aead.xchacha20poly1305ietf } @@ -78,9 +86,10 @@ extension Sodium: SodiumType { } } -extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {} -extension Sign: SignType {} +extension Box: BoxType {} extension GenericHash: GenericHashType {} +extension Sign: SignType {} +extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {} struct Ed25519Wrapper: Ed25519Type { func sign(data: Bytes, keyPair: ECKeyPair) throws -> Bytes? { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift index 8491ed794..a135ba0c1 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift @@ -3,27 +3,40 @@ import SessionUtilitiesKit import Sodium extension MessageReceiver { - - internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: ECKeyPair) throws -> (plaintext: Data, senderX25519PublicKey: String) { + internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: ECKeyPair, dependencies: Dependencies = Dependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) { let recipientX25519PrivateKey = x25519KeyPair.privateKey let recipientX25519PublicKey = Data(hex: x25519KeyPair.hexEncodedPublicKey.removingIdPrefixIfNeeded()) - let sodium = Sodium() - let signatureSize = sodium.sign.Bytes - let ed25519PublicKeySize = sodium.sign.PublicKeyBytes + let signatureSize = dependencies.sign.Bytes + let ed25519PublicKeySize = dependencies.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 } + guard + let plaintextWithMetadata = dependencies.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.. Data { - guard let userED25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else { + internal static func encryptWithSessionProtocol(_ plaintext: Data, for recipientHexEncodedX25519PublicKey: String, using dependencies: Dependencies = Dependencies()) throws -> Data { + guard let userED25519KeyPair = dependencies.storage.getUserED25519KeyPair() else { throw Error.noUserED25519KeyPair } let recipientX25519PublicKey = Data(hex: recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded()) - let sodium = Sodium() let verificationData = plaintext + Data(userED25519KeyPair.publicKey) + recipientX25519PublicKey - guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { + guard let signature = dependencies.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed } let plaintextWithMetadata = plaintext + Data(userED25519KeyPair.publicKey) + Data(signature) - guard let ciphertext = sodium.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else { + guard let ciphertext = dependencies.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else { throw Error.encryptionFailed } @@ -26,7 +24,7 @@ extension MessageSender { internal static func encryptWithSessionBlindingProtocol(_ plaintext: Data, for recipientBlindedId: String, openGroupPublicKey: String, using dependencies: Dependencies = Dependencies()) throws -> Data { guard SessionId.Prefix(from: recipientBlindedId) == .blinded else { throw Error.signingFailed } - guard let userEd25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else { + guard let userEd25519KeyPair = dependencies.storage.getUserED25519KeyPair() else { throw Error.noUserED25519KeyPair } guard let blindedKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, genericHash: dependencies.genericHash) else { diff --git a/SessionMessagingKit/Utilities/ContactUtilities.swift b/SessionMessagingKit/Utilities/ContactUtilities.swift index 80a730eb8..7aeac54d7 100644 --- a/SessionMessagingKit/Utilities/ContactUtilities.swift +++ b/SessionMessagingKit/Utilities/ContactUtilities.swift @@ -73,7 +73,7 @@ public enum ContactUtilities { // Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match ContactUtilities.enumerateApprovedContactThreads(using: transaction) { contactThread, contact, stop in - guard dependencies.sodium.sessionId(contact.sessionID, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { + guard dependencies.sodium.sessionId(contact.sessionID, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey, genericHash: dependencies.genericHash) else { return } @@ -91,7 +91,7 @@ public enum ContactUtilities { // a thread with this contact in a different SOGS and had cached the mapping) dependencies.storage.enumerateBlindedIdMapping(using: transaction) { mapping, stop in guard mapping.serverPublicKey != serverPublicKey else { return } - guard dependencies.sodium.sessionId(mapping.sessionId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { + guard dependencies.sodium.sessionId(mapping.sessionId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey, genericHash: dependencies.genericHash) else { return } diff --git a/SessionMessagingKit/Utilities/Dependencies.swift b/SessionMessagingKit/Utilities/Dependencies.swift index 01273c567..7eb2b56fe 100644 --- a/SessionMessagingKit/Utilities/Dependencies.swift +++ b/SessionMessagingKit/Utilities/Dependencies.swift @@ -30,10 +30,16 @@ public class Dependencies { set { _sodium = newValue } } - internal var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? - public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { - get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } - set { _aeadXChaCha20Poly1305Ietf = newValue } + internal var _box: BoxType? + public var box: BoxType { + get { Dependencies.getValueSettingIfNull(&_box) { sodium.getBox() } } + set { _box = newValue } + } + + internal var _genericHash: GenericHashType? + public var genericHash: GenericHashType { + get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } + set { _genericHash = newValue } } internal var _sign: SignType? @@ -42,10 +48,10 @@ public class Dependencies { set { _sign = newValue } } - internal var _genericHash: GenericHashType? - public var genericHash: GenericHashType { - get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } - set { _genericHash = newValue } + internal var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? + public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { + get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } + set { _aeadXChaCha20Poly1305Ietf = newValue } } internal var _ed25519: Ed25519Type? @@ -85,9 +91,10 @@ public class Dependencies { identityManager: IdentityManagerProtocol? = nil, storage: SessionMessagingKitStorageProtocol? = nil, sodium: SodiumType? = nil, - aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, - sign: SignType? = nil, + box: BoxType? = nil, genericHash: GenericHashType? = nil, + sign: SignType? = nil, + aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, ed25519: Ed25519Type? = nil, nonceGenerator16: NonceGenerator16ByteType? = nil, nonceGenerator24: NonceGenerator24ByteType? = nil, @@ -98,9 +105,10 @@ public class Dependencies { _identityManager = identityManager _storage = storage _sodium = sodium - _aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf - _sign = sign + _box = box _genericHash = genericHash + _sign = sign + _aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf _ed25519 = ed25519 _nonceGenerator16 = nonceGenerator16 _nonceGenerator24 = nonceGenerator24 diff --git a/SessionMessagingKit/Utilities/Sodium+Utilities.swift b/SessionMessagingKit/Utilities/Sodium+Utilities.swift index 0409c6623..feddd0df4 100644 --- a/SessionMessagingKit/Utilities/Sodium+Utilities.swift +++ b/SessionMessagingKit/Utilities/Sodium+Utilities.swift @@ -41,6 +41,15 @@ extension Sign { } } +/// These extenion methods are used to generate a sign "blinded" messages +/// +/// According to the Swift engineers the only situation when `UnsafeRawBufferPointer.baseAddress` is nil is when it's an +/// empty collection; as such our guard cases wihch return `-1` when unwrapping this value should never be hit and we can ignore +/// them as possible results. +/// +/// For more information see: +/// https://forums.swift.org/t/when-is-unsafemutablebufferpointer-baseaddress-nil/32136/5 +/// https://github.com/apple/swift-evolution/blob/master/proposals/0055-optional-unsafe-pointers.md#unsafebufferpointer extension Sodium { private static let scalarLength: Int = Int(crypto_core_ed25519_scalarbytes()) // 32 private static let noClampLength: Int = Int(crypto_scalarmult_ed25519_bytes()) // 32 @@ -49,7 +58,7 @@ extension Sodium { private static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64 /// 64-byte blake2b hash then reduce to get the blinding factor - public func generateBlindingFactor(serverPublicKey: String) -> Bytes? { + public func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? { /// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest()) guard let serverPubKeyData: Data = serverPublicKey.dataFromHex() else { return nil } guard let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else { @@ -59,18 +68,15 @@ extension Sodium { /// Reduce the server public key into an ed25519 scalar (`k`) let kPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.scalarLength) - let kResult = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in + _ = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in guard let serverPublicKeyHashBaseAddress: UnsafePointer = serverPublicKeyHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } crypto_core_ed25519_scalar_reduce(kPtr, serverPublicKeyHashBaseAddress) return 0 } - /// Ensure the above worked - guard kResult == 0 else { return nil } - return Data(bytes: kPtr, count: Sodium.scalarLength).bytes } @@ -78,21 +84,20 @@ extension Sodium { /// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the /// same secret scalar secret (and so this is just the most convenient way to get 'a' out of /// a sodium Ed25519 secret key) - private func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes? { + func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes { /// a = s.to_curve25519_private_key().encode() let aPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.scalarMultLength) - let aResult = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in + /// Looks like the `crypto_sign_ed25519_sk_to_curve25519` function can't actually fail so no need to verify the result + /// See: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L70 + _ = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in guard let secretKeyBaseAddress: UnsafePointer = secretKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } return crypto_sign_ed25519_sk_to_curve25519(aPtr, secretKeyBaseAddress) } - /// Ensure the above worked - guard aResult == 0 else { return nil } - return Data(bytes: aPtr, count: Sodium.scalarMultLength).bytes } @@ -101,20 +106,22 @@ extension Sodium { guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else { return nil } - guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey) else { return nil } - guard let aBytes: Bytes = generatePrivateKeyScalar(secretKey: edKeyPair.secretKey) else { return nil } + guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey, genericHash: genericHash) else { + return nil + } + let aBytes: Bytes = generatePrivateKeyScalar(secretKey: edKeyPair.secretKey) /// Generate the blinded key pair `ka`, `kA` let kaPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.secretKeyLength) let kAPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.publicKeyLength) - let kaResult = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in + _ = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in return kBytes.withUnsafeBytes { (kPtr: UnsafeRawBufferPointer) -> Int32 in guard let kBaseAddress: UnsafePointer = kPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } guard let aBaseAddress: UnsafePointer = aPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } crypto_core_ed25519_scalar_mul(kaPtr, kBaseAddress, aBaseAddress) @@ -122,9 +129,6 @@ extension Sodium { } } - /// Ensure the above worked - guard kaResult == 0 else { return nil } - guard crypto_scalarmult_ed25519_base_noclamp(kAPtr, kaPtr) == 0 else { return nil } return Box.KeyPair( @@ -144,18 +148,15 @@ extension Sodium { let combinedHashBytes: Bytes = (H_rh + kA + message).sha512() let rPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.scalarLength) - let rResult = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in + _ = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in guard let combinedHashBaseAddress: UnsafePointer = combinedHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } crypto_core_ed25519_scalar_reduce(rPtr, combinedHashBaseAddress) return 0 } - /// Ensure the above worked - guard rResult == 0 else { return nil } - /// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r) let sig_RPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.noClampLength) guard crypto_scalarmult_ed25519_base_noclamp(sig_RPtr, rPtr) == 0 else { return nil } @@ -165,25 +166,22 @@ extension Sodium { let HRAMHashBytes: Bytes = (sig_RBytes + kA + message).sha512() let HRAMPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.scalarLength) - let HRAMResult = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in + _ = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in guard let HRAMHashBaseAddress: UnsafePointer = HRAMHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } crypto_core_ed25519_scalar_reduce(HRAMPtr, HRAMHashBaseAddress) return 0 } - /// Ensure the above worked - guard HRAMResult == 0 else { return nil } - /// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka)) let sig_sMulPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.scalarLength) let sig_sPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.scalarLength) - let sig_sResult = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in + _ = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in guard let kaBaseAddress: UnsafePointer = kaPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } crypto_core_ed25519_scalar_mul(sig_sMulPtr, HRAMPtr, kaBaseAddress) @@ -191,8 +189,6 @@ extension Sodium { return 0 } - guard sig_sResult == 0 else { return nil } - /// full_sig = sig_R + sig_s return (Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes + Data(bytes: sig_sPtr, count: Sodium.scalarLength).bytes) } @@ -204,10 +200,10 @@ extension Sodium { let result = rhsKeyBytes.withUnsafeBytes { (rhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in return lhsKeyBytes.withUnsafeBytes { (lhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in guard let lhsKeyBytesBaseAddress: UnsafePointer = lhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } guard let rhsKeyBytesBaseAddress: UnsafePointer = rhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return -1 + return -1 // Impossible case (refer to comments at top of extension) } return crypto_scalarmult_ed25519_noclamp(combinedPtr, lhsKeyBytesBaseAddress, rhsKeyBytesBaseAddress) @@ -228,18 +224,23 @@ extension Sodium { /// /// BLAKE2b(b kA || kA || kB) public func sharedBlindedEncryptionKey(secretKey: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? { - guard let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey) else { return nil } - guard let combinedKeyBytes: Bytes = combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey) else { return nil } + let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey) + + guard let combinedKeyBytes: Bytes = combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey) else { + return nil + } return genericHash.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32) } /// This method should be used to check if a users standard sessionId matches a blinded one - public func sessionId(_ standardSessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool { + public func sessionId(_ standardSessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool { // Only support generating blinded keys for standard session ids guard let sessionId: SessionId = SessionId(from: standardSessionId), sessionId.prefix == .standard else { return false } guard let blindedId: SessionId = SessionId(from: blindedSessionId), blindedId.prefix == .blinded else { return false } - guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey) else { return false } + guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey, genericHash: genericHash) else { + return false + } /// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is the positive (which is what /// Signal's XEd25519 conversion always uses) diff --git a/SessionMessagingKitTests/Common Networking/HeaderSpec.swift b/SessionMessagingKitTests/Common Networking/HeaderSpec.swift new file mode 100644 index 000000000..df47313ff --- /dev/null +++ b/SessionMessagingKitTests/Common Networking/HeaderSpec.swift @@ -0,0 +1,20 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class HeaderSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a Dictionary of Header to String values") { + it("can be converted into a dictionary of String to String values") { + expect([Header.authorization: "test"].toHTTPHeaders()).to(equal(["Authorization": "test"])) + } + } + } +} diff --git a/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift b/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift new file mode 100644 index 000000000..499b8e658 --- /dev/null +++ b/SessionMessagingKitTests/Common Networking/Models/FileUploadResponseSpec.swift @@ -0,0 +1,32 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class FileUploadResponseSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a FileUploadResponse") { + context("when decoding") { + it("handles a string id value") { + let jsonData: Data = "{\"id\":\"123\"}".data(using: .utf8)! + let response: FileUploadResponse? = try? JSONDecoder().decode(FileUploadResponse.self, from: jsonData) + + expect(response?.id).to(equal("123")) + } + + it("handles an int id value") { + let jsonData: Data = "{\"id\":124}".data(using: .utf8)! + let response: FileUploadResponse? = try? JSONDecoder().decode(FileUploadResponse.self, from: jsonData) + + expect(response?.id).to(equal("124")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Common Networking/RequestSpec.swift b/SessionMessagingKitTests/Common Networking/RequestSpec.swift new file mode 100644 index 000000000..b23921fd3 --- /dev/null +++ b/SessionMessagingKitTests/Common Networking/RequestSpec.swift @@ -0,0 +1,167 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class RequestSpec: QuickSpec { + struct TestType: Codable, Equatable { + let stringValue: String + } + + // MARK: - Spec + + override func spec() { + describe("a Request") { + it("is initialized with the correct default values") { + let request: Request = Request( + server: "testServer", + endpoint: .batch + ) + + expect(request.method.rawValue).to(equal("GET")) + expect(request.queryParameters).to(equal([:])) + expect(request.headers).to(equal([:])) + expect(request.body).to(beNil()) + } + + context("when generating a URL") { + it("adds a leading forward slash to the endpoint path") { + let request: Request = Request( + server: "testServer", + endpoint: .batch + ) + + expect(request.urlPathAndParamsString).to(equal("/batch")) + } + + it("creates a valid URL with no query parameters") { + let request: Request = Request( + server: "testServer", + endpoint: .batch + ) + + expect(request.urlPathAndParamsString).to(equal("/batch")) + } + + it("creates a valid URL when query parameters are provided") { + let request: Request = Request( + server: "testServer", + endpoint: .batch, + queryParameters: [ + .limit: "123" + ] + ) + + expect(request.urlPathAndParamsString).to(equal("/batch?limit=123")) + } + } + + context("when generating a URLRequest") { + it("sets all the values correctly") { + let request: Request = Request( + method: .delete, + server: "testServer", + endpoint: .batch, + headers: [ + .authorization: "test" + ] + ) + let urlRequest: URLRequest? = try? request.generateUrlRequest() + + expect(urlRequest?.httpMethod).to(equal("DELETE")) + expect(urlRequest?.allHTTPHeaderFields).to(equal(["Authorization": "test"])) + expect(urlRequest?.httpBody).to(beNil()) + } + + it("throws an error if the URL is invalid") { + let request: Request = Request( + server: "testServer", + endpoint: .roomPollInfo("!!%%", 123) + ) + + expect { + try request.generateUrlRequest() + } + .to(throwError(HTTP.Error.invalidURL)) + } + + context("with a base64 string body") { + it("successfully encodes the body") { + let request: Request = Request( + server: "testServer", + endpoint: .batch, + body: "TestMessage".data(using: .utf8)!.base64EncodedString() + ) + + let urlRequest: URLRequest? = try? request.generateUrlRequest() + let requestBody: Data? = Data(base64Encoded: urlRequest?.httpBody?.base64EncodedString() ?? "") + let requestBodyString: String? = String(data: requestBody ?? Data(), encoding: .utf8) + + expect(requestBodyString).to(equal("TestMessage")) + } + + it("throws an error if the body is not base64 encoded") { + let request: Request = Request( + server: "testServer", + endpoint: .batch, + body: "TestMessage" + ) + + expect { + try request.generateUrlRequest() + } + .to(throwError(HTTP.Error.parsingFailed)) + } + } + + context("with a byte body") { + it("successfully encodes the body") { + let request: Request<[UInt8], OpenGroupAPI.Endpoint> = Request( + server: "testServer", + endpoint: .batch, + body: [1, 2, 3] + ) + + let urlRequest: URLRequest? = try? request.generateUrlRequest() + + expect(urlRequest?.httpBody?.bytes).to(equal([1, 2, 3])) + } + } + + context("with a JSON body") { + it("successfully encodes the body") { + let request: Request = Request( + server: "testServer", + endpoint: .batch, + body: TestType(stringValue: "test") + ) + + let urlRequest: URLRequest? = try? request.generateUrlRequest() + let requestBody: TestType? = try? JSONDecoder().decode( + TestType.self, + from: urlRequest?.httpBody ?? Data() + ) + + expect(requestBody).to(equal(TestType(stringValue: "test"))) + } + + it("successfully encodes no body") { + let request: Request = Request( + server: "testServer", + endpoint: .batch, + body: nil + ) + + expect { + try request.generateUrlRequest() + }.toNot(throwError()) + } + } + } + } + } +} diff --git a/SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift b/SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift new file mode 100644 index 000000000..3c2c26d40 --- /dev/null +++ b/SessionMessagingKitTests/Contacts/BlindedIdMappingSpec.swift @@ -0,0 +1,48 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class BlindedIdMappingSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + describe("a BlindedIdMapping") { + context("when initializing") { + it("sets the values correctly") { + let mapping: BlindedIdMapping = BlindedIdMapping( + blindedId: "testBlindedId", + sessionId: "testSessionId", + serverPublicKey: "testPublicKey" + ) + + expect(mapping.blindedId).to(equal("testBlindedId")) + expect(mapping.sessionId).to(equal("testSessionId")) + expect(mapping.serverPublicKey).to(equal("testPublicKey")) + } + } + + context("when NSCoding") { + // Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable + it("successfully encodes and decodes") { + let mappingToEncode: BlindedIdMapping = BlindedIdMapping( + blindedId: "testBlindedId", + sessionId: "testSessionId", + serverPublicKey: "testPublicKey" + ) + let encodedData: Data = try! NSKeyedArchiver.archivedData(withRootObject: mappingToEncode, requiringSecureCoding: false) + let mapping: BlindedIdMapping? = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedData) as? BlindedIdMapping + + expect(mapping).toNot(beNil()) + expect(mapping?.blindedId).to(equal("testBlindedId")) + expect(mapping?.sessionId).to(equal("testSessionId")) + expect(mapping?.serverPublicKey).to(equal("testPublicKey")) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift b/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift new file mode 100644 index 000000000..f988057a8 --- /dev/null +++ b/SessionMessagingKitTests/Open Groups/Models/BatchRequestInfoSpec.swift @@ -0,0 +1,385 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import PromiseKit +import SessionSnodeKit + +import Quick +import Nimble + +@testable import SessionMessagingKit +import AVFoundation + +class BatchRequestInfoSpec: QuickSpec { + struct TestType: Codable, Equatable { + let stringValue: String + } + + // MARK: - Spec + + override func spec() { + // MARK: - BatchSubRequest + + describe("a BatchSubRequest") { + var subRequest: OpenGroupAPI.BatchSubRequest! + + context("when initializing") { + it("sets the headers to nil if there aren't any") { + subRequest = OpenGroupAPI.BatchSubRequest( + request: Request( + server: "testServer", + endpoint: .batch + ) + ) + + expect(subRequest.headers).to(beNil()) + } + + it("converts the headers to HTTP headers") { + subRequest = OpenGroupAPI.BatchSubRequest( + request: Request( + method: .get, + server: "testServer", + endpoint: .batch, + queryParameters: [:], + headers: [.authorization: "testAuth"], + body: nil + ) + ) + + expect(subRequest.headers).to(equal(["Authorization": "testAuth"])) + } + } + + context("when encoding") { + it("successfully encodes a string body") { + subRequest = OpenGroupAPI.BatchSubRequest( + request: Request( + method: .get, + server: "testServer", + endpoint: .batch, + queryParameters: [:], + headers: [:], + body: "testBody" + ) + ) + let subRequestData: Data = try! JSONEncoder().encode(subRequest) + let subRequestString: String? = String(data: subRequestData, encoding: .utf8) + + expect(subRequestString) + .to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"b64\":\"testBody\"}")) + } + + it("successfully encodes a byte body") { + subRequest = OpenGroupAPI.BatchSubRequest( + request: Request<[UInt8], OpenGroupAPI.Endpoint>( + method: .get, + server: "testServer", + endpoint: .batch, + queryParameters: [:], + headers: [:], + body: [1, 2, 3] + ) + ) + let subRequestData: Data = try! JSONEncoder().encode(subRequest) + let subRequestString: String? = String(data: subRequestData, encoding: .utf8) + + expect(subRequestString) + .to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"bytes\":[1,2,3]}")) + } + + it("successfully encodes a JSON body") { + subRequest = OpenGroupAPI.BatchSubRequest( + request: Request( + method: .get, + server: "testServer", + endpoint: .batch, + queryParameters: [:], + headers: [:], + body: TestType(stringValue: "testValue") + ) + ) + let subRequestData: Data = try! JSONEncoder().encode(subRequest) + let subRequestString: String? = String(data: subRequestData, encoding: .utf8) + + expect(subRequestString) + .to(equal("{\"path\":\"\\/batch\",\"method\":\"GET\",\"json\":{\"stringValue\":\"testValue\"}}")) + } + } + } + + // MARK: - BatchSubResponse + + describe("a BatchSubResponse") { + context("when decoding") { + it("decodes correctly") { + let jsonString: String = """ + { + "code": 200, + "headers": { + "testKey": "testValue" + }, + "body": { + "stringValue": "testValue" + } + } + """ + let subResponse: OpenGroupAPI.BatchSubResponse? = try? JSONDecoder().decode( + OpenGroupAPI.BatchSubResponse.self, + from: jsonString.data(using: .utf8)! + ) + + expect(subResponse).toNot(beNil()) + expect(subResponse?.body).toNot(beNil()) + } + + it("decodes with invalid body data") { + let jsonString: String = """ + { + "code": 200, + "headers": { + "testKey": "testValue" + }, + "body": "Hello!!!" + } + """ + let subResponse: OpenGroupAPI.BatchSubResponse? = try? JSONDecoder().decode( + OpenGroupAPI.BatchSubResponse.self, + from: jsonString.data(using: .utf8)! + ) + + expect(subResponse).toNot(beNil()) + } + + it("flags invalid body data as invalid") { + let jsonString: String = """ + { + "code": 200, + "headers": { + "testKey": "testValue" + }, + "body": "Hello!!!" + } + """ + let subResponse: OpenGroupAPI.BatchSubResponse? = try? JSONDecoder().decode( + OpenGroupAPI.BatchSubResponse.self, + from: jsonString.data(using: .utf8)! + ) + + expect(subResponse).toNot(beNil()) + expect(subResponse?.body).to(beNil()) + expect(subResponse?.failedToParseBody).to(beTrue()) + } + + it("does not flag a missing or invalid optional body as invalid") { + let jsonString: String = """ + { + "code": 200, + "headers": { + "testKey": "testValue" + }, + } + """ + let subResponse: OpenGroupAPI.BatchSubResponse? = try? JSONDecoder().decode( + OpenGroupAPI.BatchSubResponse.self, + from: jsonString.data(using: .utf8)! + ) + + expect(subResponse).toNot(beNil()) + expect(subResponse?.body).to(beNil()) + expect(subResponse?.failedToParseBody).to(beFalse()) + } + + it("does not flag a NoResponse body as invalid") { + let jsonString: String = """ + { + "code": 200, + "headers": { + "testKey": "testValue" + }, + } + """ + let subResponse: OpenGroupAPI.BatchSubResponse? = try? JSONDecoder().decode( + OpenGroupAPI.BatchSubResponse.self, + from: jsonString.data(using: .utf8)! + ) + + expect(subResponse).toNot(beNil()) + expect(subResponse?.body).to(beNil()) + expect(subResponse?.failedToParseBody).to(beFalse()) + } + } + } + + // MARK: - BatchRequestInfo + + describe("a BatchRequestInfo") { + var request: Request! + + beforeEach { + request = Request( + method: .get, + server: "testServer", + endpoint: .batch, + queryParameters: [:], + headers: [:], + body: TestType(stringValue: "testValue") + ) + } + + it("initializes correctly when given a request") { + let requestInfo: OpenGroupAPI.BatchRequestInfo = OpenGroupAPI.BatchRequestInfo( + request: request + ) + + expect(requestInfo.request).to(equal(request)) + expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse.self).to(beTrue()) + } + + it("initializes correctly when given a request and a response type") { + let requestInfo: OpenGroupAPI.BatchRequestInfo = OpenGroupAPI.BatchRequestInfo( + request: request, + responseType: TestType.self + ) + + expect(requestInfo.request).to(equal(request)) + expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse.self).to(beTrue()) + } + + it("exposes the endpoint correctly") { + let requestInfo: OpenGroupAPI.BatchRequestInfo = OpenGroupAPI.BatchRequestInfo( + request: request + ) + + expect(requestInfo.endpoint.path).to(equal(request.endpoint.path)) + } + + it("generates a sub request correctly") { + let requestInfo: OpenGroupAPI.BatchRequestInfo = OpenGroupAPI.BatchRequestInfo( + request: request + ) + let subRequest: OpenGroupAPI.BatchSubRequest = requestInfo.toSubRequest() + + expect(subRequest.method).to(equal(request.method)) + expect(subRequest.path).to(equal(request.urlPathAndParamsString)) + expect(subRequest.headers).to(beNil()) + } + } + + // MARK: - Convenience + // MARK: --Decodable + + describe("a Decodable") { + it("decodes correctly") { + let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)! + let result: TestType? = try? TestType.decoded(from: jsonData) + + expect(result).to(equal(TestType(stringValue: "testValue"))) + } + } + + // MARK: - --Promise + + describe("an (OnionRequestResponseInfoType, Data?) Promise") { + var responseInfo: OnionRequestResponseInfoType! + var capabilities: OpenGroupAPI.Capabilities! + var pinnedMessage: OpenGroupAPI.PinnedMessage! + var data: Data! + + beforeEach { + responseInfo = OnionRequestAPI.ResponseInfo(code: 200, headers: [:]) + capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil) + pinnedMessage = OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 123, pinnedBy: "test") + data = """ + [\([ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilities, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: pinnedMessage, + failedToParseBody: false + ) + ) + ] + .map { String(data: $0, encoding: .utf8)! } + .joined(separator: ","))] + """.data(using: .utf8)! + } + + it("decodes valid data correctly") { + let result = Promise.value((responseInfo, data)) + .decoded(as: [ + OpenGroupAPI.BatchSubResponse.self, + OpenGroupAPI.BatchSubResponse.self + ]) + + expect(result.value).toNot(beNil()) + expect((result.value?[0].1 as? OpenGroupAPI.BatchSubResponse)?.body) + .to(equal(capabilities)) + expect((result.value?[1].1 as? OpenGroupAPI.BatchSubResponse)?.body) + .to(equal(pinnedMessage)) + } + + it("fails if there is no data") { + let result = Promise.value((responseInfo, nil)).decoded(as: []) + + expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription)) + } + + it("fails if the data is not JSON") { + let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: []) + + expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription)) + } + + it("fails if the data is not a JSON array") { + let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: []) + + expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription)) + } + + it("fails if the JSON array does not have the same number of items as the expected types") { + let result = Promise.value((responseInfo, data)) + .decoded(as: [ + OpenGroupAPI.BatchSubResponse.self, + OpenGroupAPI.BatchSubResponse.self, + OpenGroupAPI.BatchSubResponse.self + ]) + + expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription)) + } + + it("fails if one of the JSON array values fails to decode") { + data = """ + [\([ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilities, + failedToParseBody: false + ) + ) + ] + .map { String(data: $0, encoding: .utf8)! } + .joined(separator: ",")),{"test": "test"}] + """.data(using: .utf8)! + let result = Promise.value((responseInfo, data)) + .decoded(as: [ + OpenGroupAPI.BatchSubResponse.self, + OpenGroupAPI.BatchSubResponse.self + ]) + + expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription)) + } + } + } +} diff --git a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift index b6d310e8b..17cd0e496 100644 --- a/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/OpenGroupSpec.swift @@ -29,7 +29,7 @@ class OpenGroupSpec: QuickSpec { } context("when NSCoding") { - // Note: Unit testing NSCoder is horrible so we won't do it - wait until we refactor it to Codable + // Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable it("successfully encodes and decodes") { let openGroupToEncode: OpenGroup = OpenGroup( server: "server", diff --git a/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift b/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift index ccd8557d0..6b5d47a4e 100644 --- a/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/Models/ServerSpec.swift @@ -24,7 +24,7 @@ class ServerSpec: QuickSpec { } context("when NSCoding") { - // Note: Unit testing NSCoder is horrible so we won't do it - wait until we refactor it to Codable + // Note: Unit testing NSCoder is horrible so we won't do it properly - wait until we refactor it to Codable it("successfully encodes and decodes") { let serverToEncode: OpenGroupAPI.Server = OpenGroupAPI.Server( name: "test", diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 94bc382d8..cbf9262c9 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -43,9 +43,9 @@ class OpenGroupAPISpec: QuickSpec { onionApi: TestOnionRequestAPI.self, storage: mockStorage, sodium: mockSodium, - aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf, - sign: mockSign, genericHash: mockGenericHash, + sign: mockSign, + aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf, ed25519: mockEd25519, nonceGenerator16: mockNonce16Generator, nonceGenerator24: mockNonce24Generator, @@ -229,7 +229,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestData?.urlString).to(equal("testServer/batch")) expect(requestData?.httpMethod).to(equal("POST")) expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) } it("retrieves recent messages if there was no last message") { @@ -2663,10 +2663,10 @@ class OpenGroupAPISpec: QuickSpec { expect(requestData?.urlString).to(equal("testServer/rooms")) expect(requestData?.httpMethod).to(equal("GET")) expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) expect(requestData?.headers).to(haveCount(4)) expect(requestData?.headers[Header.sogsPubKey.rawValue]) - .to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + .to(equal("0088672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSignature".bytes.toBase64())) @@ -2720,9 +2720,9 @@ class OpenGroupAPISpec: QuickSpec { expect(requestData?.urlString).to(equal("testServer/rooms")) expect(requestData?.httpMethod).to(equal("GET")) expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) expect(requestData?.headers).to(haveCount(4)) - expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890")) expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg==")) expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSogsSignature".bytes.toBase64())) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index c7c7b863a..dfdc6b0ef 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -114,9 +114,9 @@ class OpenGroupManagerSpec: QuickSpec { identityManager: mockIdentityManager, storage: mockStorage, sodium: mockSodium, - aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf, - sign: mockSign, genericHash: mockGenericHash, + sign: mockSign, + aeadXChaCha20Poly1305Ietf: mockAeadXChaCha20Poly1305Ietf, ed25519: MockEd25519(), nonceGenerator16: mockNonce16Generator, nonceGenerator24: mockNonce24Generator, @@ -1600,7 +1600,7 @@ class OpenGroupManagerSpec: QuickSpec { expect(didComplete).toEventually(beTrue(), timeout: .milliseconds(50)) expect(mockStorage) - .to(call(matchingParameters: true) { + .toEventually(call(matchingParameters: true) { $0.setOpenGroup( OpenGroup( server: "testServer", @@ -1620,6 +1620,11 @@ class OpenGroupManagerSpec: QuickSpec { beNil(), timeout: .milliseconds(50) ) + expect(mockOGMCache) + .toEventually( + call(.exactly(times: 1)) { $0.groupImagePromises }, + timeout: .milliseconds(50) + ) expect(testGroupThread.numSaveCalls) .toEventually( equal(2), // Call to save the open group and then to save the image @@ -2121,7 +2126,7 @@ class OpenGroupManagerSpec: QuickSpec { } .thenReturn([]) mockSodium - .when { $0.generateBlindingFactor(serverPublicKey: any()) } + .when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) } .thenReturn([]) mockAeadXChaCha20Poly1305Ietf .when { diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift new file mode 100644 index 000000000..c59f20e23 --- /dev/null +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift @@ -0,0 +1,500 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class MessageReceiverDecryptionSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + var mockStorage: MockStorage! + var mockSodium: MockSodium! + var mockBox: MockBox! + var mockGenericHash: MockGenericHash! + var mockSign: MockSign! + var mockAeadXChaCha: MockAeadXChaCha20Poly1305Ietf! + var mockNonce24Generator: MockNonce24Generator! + var dependencies: Dependencies! + + describe("a MessageReceiver") { + beforeEach { + mockStorage = MockStorage() + mockSodium = MockSodium() + mockBox = MockBox() + mockGenericHash = MockGenericHash() + mockSign = MockSign() + mockAeadXChaCha = MockAeadXChaCha20Poly1305Ietf() + mockNonce24Generator = MockNonce24Generator() + + mockAeadXChaCha + .when { $0.encrypt(message: anyArray(), secretKey: anyArray(), nonce: anyArray()) } + .thenReturn(nil) + + dependencies = Dependencies( + storage: mockStorage, + sodium: mockSodium, + box: mockBox, + genericHash: mockGenericHash, + sign: mockSign, + aeadXChaCha20Poly1305Ietf: mockAeadXChaCha, + nonceGenerator24: mockNonce24Generator + ) + + mockStorage + .when { $0.getUserED25519KeyPair() } + .thenReturn( + Box.KeyPair( + publicKey: Data(hex: TestConstants.edPublicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + mockBox + .when { + $0.open( + anonymousCipherText: anyArray(), + recipientPublicKey: anyArray(), + recipientSecretKey: anyArray() + ) + } + .thenReturn([UInt8](repeating: 0, count: 100)) + mockSodium + .when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) } + .thenReturn( + Box.KeyPair( + publicKey: Data(hex: TestConstants.blindedPublicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + mockSodium + .when { + $0.sharedBlindedEncryptionKey( + secretKey: anyArray(), + otherBlindedPublicKey: anyArray(), + fromBlindedPublicKey: anyArray(), + toBlindedPublicKey: anyArray(), + genericHash: mockGenericHash + ) + } + .thenReturn([]) + mockSodium + .when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) } + .thenReturn([]) + mockSodium + .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } + .thenReturn(Data(hex: TestConstants.blindedPublicKey).bytes) + mockSign + .when { $0.toX25519(ed25519PublicKey: anyArray()) } + .thenReturn(Data(hex: TestConstants.publicKey).bytes) + mockSign + .when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) } + .thenReturn(true) + mockAeadXChaCha + .when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) } + .thenReturn("TestMessage".data(using: .utf8)!.bytes + [UInt8](repeating: 0, count: 32)) + mockNonce24Generator + .when { $0.nonce() } + .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) + } + + context("when decrypting with the session protocol") { + it("successfully decrypts a message") { + let result = try? MessageReceiver.decryptWithSessionProtocol( + ciphertext: Data( + base64Encoded: "SRP0eBUWh4ez6ppWjUs5/Wph5fhnPRgB5zsWWnTz+FBAw/YI3oS2pDpIfyetMTbU" + + "sFMhE5G4PbRtQFey1hsxLl221Qivc3ayaX2Mm/X89Dl8e45BC+Lb/KU9EdesxIK4pVgYXs9XrMtX3v8" + + "dt0eBaXneOBfr7qB8pHwwMZjtkOu1ED07T9nszgbWabBphUfWXe2U9K3PTRisSCI=" + )!, + using: try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ), + dependencies: Dependencies() + ) + + expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage")) + expect(result?.senderX25519PublicKey) + .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) + } + + it("throws an error if it cannot open the message") { + mockBox + .when { + $0.open( + anonymousCipherText: anyArray(), + recipientPublicKey: anyArray(), + recipientSecretKey: anyArray() + ) + } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionProtocol( + ciphertext: "TestMessage".data(using: .utf8)!, + using: try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ), + dependencies: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if the open message is too short") { + mockBox + .when { + $0.open( + anonymousCipherText: anyArray(), + recipientPublicKey: anyArray(), + recipientSecretKey: anyArray() + ) + } + .thenReturn([1, 2, 3]) + + expect { + try MessageReceiver.decryptWithSessionProtocol( + ciphertext: "TestMessage".data(using: .utf8)!, + using: try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ), + dependencies: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if it cannot verify the message") { + mockSign + .when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) } + .thenReturn(false) + + expect { + try MessageReceiver.decryptWithSessionProtocol( + ciphertext: "TestMessage".data(using: .utf8)!, + using: try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ), + dependencies: dependencies + ) + } + .to(throwError(MessageReceiver.Error.invalidSignature)) + } + + it("throws an error if it cannot get the senders x25519 public key") { + mockSign.when { $0.toX25519(ed25519PublicKey: anyArray()) }.thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionProtocol( + ciphertext: "TestMessage".data(using: .utf8)!, + using: try! ECKeyPair( + publicKeyData: Data.data(fromHex: TestConstants.publicKey)!, + privateKeyData: Data.data(fromHex: TestConstants.privateKey)! + ), + dependencies: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + } + + context("when decrypting with the blinded session protocol") { + it("successfully decrypts a message") { + let result = try? MessageReceiver.decryptWithSessionBlindingProtocol( + data: Data( + hex: "00db16b6687382811d69875a5376f66acad9c49fe5e26bcf770c7e6e9c230299" + + "f61b315299dd1fa700dd7f34305c0465af9e64dc791d7f4123f1eeafa5b4d48b3ade4" + + "f4b2a2764762e5a2c7900f254bd91633b43" + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: Dependencies() + ) + + expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage")) + expect(result?.senderX25519PublicKey) + .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) + } + + it("successfully decrypts a mocked incoming message") { + let result = try? MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: false, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + + expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage")) + expect(result?.senderX25519PublicKey) + .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) + } + + it("throws an error if the data is too short") { + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: Data([1, 2, 3]), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if it cannot get the blinded keyPair") { + mockSodium + .when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if it cannot get the decryption key") { + mockSodium + .when { + $0.sharedBlindedEncryptionKey( + secretKey: anyArray(), + otherBlindedPublicKey: anyArray(), + fromBlindedPublicKey: anyArray(), + toBlindedPublicKey: anyArray(), + genericHash: mockGenericHash + ) + } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if the data version is not 0") { + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([1]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if it cannot decrypt the data") { + mockAeadXChaCha + .when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if the inner bytes are too short") { + mockAeadXChaCha + .when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) } + .thenReturn([1, 2, 3]) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + + it("throws an error if it cannot generate the blinding factor") { + mockSodium + .when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.invalidSignature)) + } + + it("throws an error if it cannot generate the combined key") { + mockSodium + .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.invalidSignature)) + } + + it("throws an error if the combined key does not match kA") { + mockSodium + .when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) } + .thenReturn(Data(hex: TestConstants.publicKey).bytes) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.invalidSignature)) + } + + it("throws an error if it cannot get the senders x25519 public key") { + mockSign + .when { $0.toX25519(ed25519PublicKey: anyArray()) } + .thenReturn(nil) + + expect { + try MessageReceiver.decryptWithSessionBlindingProtocol( + data: ( + Data([0]) + + "TestMessage".data(using: .utf8)! + + Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")! + ), + isOutgoing: true, + otherBlindedPublicKey: "15\(TestConstants.blindedPublicKey)", + with: TestConstants.serverPublicKey, + userEd25519KeyPair: Box.KeyPair( + publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, + secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes + ), + using: dependencies + ) + } + .to(throwError(MessageReceiver.Error.decryptionFailed)) + } + } + } + } +} diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift new file mode 100644 index 000000000..77b888308 --- /dev/null +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift @@ -0,0 +1,272 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium + +import Quick +import Nimble + +@testable import SessionMessagingKit + +class MessageSenderEncryptionSpec: QuickSpec { + // MARK: - Spec + + override func spec() { + var mockStorage: MockStorage! + var mockBox: MockBox! + var mockSign: MockSign! + var mockNonce24Generator: MockNonce24Generator! + var dependencies: Dependencies! + + describe("a MessageSender") { + beforeEach { + mockStorage = MockStorage() + mockBox = MockBox() + mockSign = MockSign() + mockNonce24Generator = MockNonce24Generator() + + dependencies = Dependencies( + storage: mockStorage, + box: mockBox, + sign: mockSign, + nonceGenerator24: mockNonce24Generator + ) + + mockStorage.when { $0.getUserED25519KeyPair() } + .thenReturn( + Box.KeyPair( + publicKey: Data(hex: TestConstants.edPublicKey).bytes, + secretKey: Data(hex: TestConstants.edSecretKey).bytes + ) + ) + mockNonce24Generator + .when { $0.nonce() } + .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) + } + + context("when encrypting with the session protocol") { + beforeEach { + mockBox.when { $0.seal(message: anyArray(), recipientPublicKey: anyArray()) }.thenReturn([1, 2, 3]) + mockSign.when { $0.signature(message: anyArray(), secretKey: anyArray()) }.thenReturn([]) + } + + it("can encrypt correctly") { + let result = try? MessageSender.encryptWithSessionProtocol( + "TestMessage".data(using: .utf8)!, + for: "05\(TestConstants.publicKey)", + using: Dependencies(storage: mockStorage) + ) + + // Note: A Nonce is used for this so we can't compare the exact value when not mocked + expect(result).toNot(beNil()) + expect(result?.count).to(equal(155)) + } + + it("returns the correct value when mocked") { + let result = try? MessageSender.encryptWithSessionProtocol( + "TestMessage".data(using: .utf8)!, + for: "05\(TestConstants.publicKey)", + using: dependencies + ) + + expect(result?.bytes).to(equal([1, 2, 3])) + } + + it("throws an error if there is no ed25519 keyPair") { + mockStorage.when { $0.getUserED25519KeyPair() }.thenReturn(nil) + + expect { + try MessageSender.encryptWithSessionProtocol( + "TestMessage".data(using: .utf8)!, + for: "05\(TestConstants.publicKey)", + using: dependencies + ) + } + .to(throwError(MessageSender.Error.noUserED25519KeyPair)) + } + + it("throws an error if the signature generation fails") { + mockSign.when { $0.signature(message: anyArray(), secretKey: anyArray()) }.thenReturn(nil) + + expect { + try MessageSender.encryptWithSessionProtocol( + "TestMessage".data(using: .utf8)!, + for: "05\(TestConstants.publicKey)", + using: dependencies + ) + } + .to(throwError(MessageSender.Error.signingFailed)) + } + + it("throws an error if the encryption fails") { + mockBox.when { $0.seal(message: anyArray(), recipientPublicKey: anyArray()) }.thenReturn(nil) + + expect { + try MessageSender.encryptWithSessionProtocol( + "TestMessage".data(using: .utf8)!, + for: "05\(TestConstants.publicKey)", + using: dependencies + ) + } + .to(throwError(MessageSender.Error.encryptionFailed)) + } + } + + context("when encrypting with the blinded session protocol") { + it("successfully encrypts") { + let result = try? MessageSender.encryptWithSessionBlindingProtocol( + "TestMessage".data(using: .utf8)!, + for: "15\(TestConstants.blindedPublicKey)", + openGroupPublicKey: TestConstants.serverPublicKey, + using: dependencies + ) + + expect(result?.toHexString()) + .to(equal( + "00db16b6687382811d69875a5376f66acad9c49fe5e26bcf770c7e6e9c230299" + + "f61b315299dd1fa700dd7f34305c0465af9e64dc791d7f4123f1eeafa5b4d48b" + + "3ade4f4b2a2764762e5a2c7900f254bd91633b43" + )) + } + + it("includes a version at the start of the encrypted value") { + let result = try? MessageSender.encryptWithSessionBlindingProtocol( + "TestMessage".data(using: .utf8)!, + for: "15\(TestConstants.blindedPublicKey)", + openGroupPublicKey: TestConstants.serverPublicKey, + using: dependencies + ) + + expect(result?.toHexString().prefix(2)).to(equal("00")) + } + + it("includes the nonce at the end of the encrypted value") { + let maybeResult = try? MessageSender.encryptWithSessionBlindingProtocol( + "TestMessage".data(using: .utf8)!, + for: "15\(TestConstants.blindedPublicKey)", + openGroupPublicKey: TestConstants.serverPublicKey, + using: dependencies + ) + let result: [UInt8] = (maybeResult?.bytes ?? []) + let nonceBytes: [UInt8] = Array(result[max(0, (result.count - 24)).., BoxType { + func seal(message: Bytes, recipientPublicKey: Bytes) -> Bytes? { + return accept(args: [message, recipientPublicKey]) as? Bytes + } + + func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Bytes? { + return accept(args: [anonymousCipherText, recipientPublicKey, recipientSecretKey]) as? Bytes + } +} diff --git a/SessionMessagingKitTests/_TestUtilities/MockSign.swift b/SessionMessagingKitTests/_TestUtilities/MockSign.swift index 19f3b00de..98f2887db 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockSign.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockSign.swift @@ -7,6 +7,7 @@ import Sodium @testable import SessionMessagingKit class MockSign: Mock, SignType { + var Bytes: Int = 64 var PublicKeyBytes: Int = 32 func signature(message: Bytes, secretKey: Bytes) -> Bytes? { diff --git a/SessionMessagingKitTests/_TestUtilities/MockSodium.swift b/SessionMessagingKitTests/_TestUtilities/MockSodium.swift index 502bec493..4ed5bb75f 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockSodium.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockSodium.swift @@ -7,12 +7,13 @@ import Sodium @testable import SessionMessagingKit class MockSodium: Mock, SodiumType { + func getBox() -> BoxType { return accept() as! BoxType } func getGenericHash() -> GenericHashType { return accept() as! GenericHashType } - func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return accept() as! AeadXChaCha20Poly1305IetfType } func getSign() -> SignType { return accept() as! SignType } + func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return accept() as! AeadXChaCha20Poly1305IetfType } - func generateBlindingFactor(serverPublicKey: String) -> Bytes? { - return accept(args: [serverPublicKey]) as? Bytes + func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? { + return accept(args: [serverPublicKey, genericHash]) as? Bytes } func blindedKeyPair(serverPublicKey: String, edKeyPair: Box.KeyPair, genericHash: GenericHashType) -> Box.KeyPair? { @@ -31,7 +32,7 @@ class MockSodium: Mock, SodiumType { return accept(args: [a, otherBlindedPublicKey, kA, kB, genericHash]) as? Bytes } - func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool { - return accept(args: [sessionId, blindedSessionId, serverPublicKey]) as! Bool + func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool { + return accept(args: [sessionId, blindedSessionId, serverPublicKey, genericHash]) as! Bool } } diff --git a/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift b/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift index 0685ea246..fce02cfeb 100644 --- a/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift +++ b/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift @@ -12,9 +12,10 @@ extension OpenGroupManager.OGMDependencies { identityManager: IdentityManagerProtocol? = nil, storage: SessionMessagingKitStorageProtocol? = nil, sodium: SodiumType? = nil, - aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, - sign: SignType? = nil, + box: BoxType? = nil, genericHash: GenericHashType? = nil, + sign: SignType? = nil, + aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, ed25519: Ed25519Type? = nil, nonceGenerator16: NonceGenerator16ByteType? = nil, nonceGenerator24: NonceGenerator24ByteType? = nil, @@ -27,9 +28,10 @@ extension OpenGroupManager.OGMDependencies { identityManager: (identityManager ?? self._identityManager), storage: (storage ?? self._storage), sodium: (sodium ?? self._sodium), - aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), - sign: (sign ?? self._sign), + box: (box ?? self._box), genericHash: (genericHash ?? self._genericHash), + sign: (sign ?? self._sign), + aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), ed25519: (ed25519 ?? self._ed25519), nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), diff --git a/SessionUtilitiesKit/General/String+Encoding.swift b/SessionUtilitiesKit/General/String+Encoding.swift index bb208adec..a794b2146 100644 --- a/SessionUtilitiesKit/General/String+Encoding.swift +++ b/SessionUtilitiesKit/General/String+Encoding.swift @@ -11,6 +11,7 @@ extension String { .map { index -> String in String(chars[index]) + String(chars[index + 1]) } .compactMap { (str: String) -> UInt8? in UInt8(str, radix: 16) } + guard bytes.count > 0 else { return nil } guard (self.count / bytes.count) == 2 else { return nil } return Data(bytes) diff --git a/SessionUtilitiesKitTests/General/SessionIdSpec.swift b/SessionUtilitiesKitTests/General/SessionIdSpec.swift index 99124712a..c3f22512a 100644 --- a/SessionUtilitiesKitTests/General/SessionIdSpec.swift +++ b/SessionUtilitiesKitTests/General/SessionIdSpec.swift @@ -42,11 +42,11 @@ class SessionIdSpec: QuickSpec { it("generates the correct hex string") { expect(SessionId(.unblinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) - .to(equal("007aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + .to(equal("0088672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) expect(SessionId(.standard, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) - .to(equal("057aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) expect(SessionId(.blinded, publicKey: Data(hex: TestConstants.publicKey).bytes).hexString) - .to(equal("157aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + .to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) } } diff --git a/SharedTest/TestConstants.swift b/SharedTest/TestConstants.swift index 3e2bb9ef0..1b9fe29f5 100644 --- a/SharedTest/TestConstants.swift +++ b/SharedTest/TestConstants.swift @@ -3,8 +3,12 @@ import Foundation enum TestConstants { - // Test Private key, not actually used (from here https://www.notion.so/oxen/SOGS-Authentication-dc64cc846cb24b2abbf7dd4bfd74abbb) - static let publicKey: String = "7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d" - static let privateKey: String = "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4" - static let edSecretKey: String = "881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4881132ee03dbd2da065aa4c94f96081f62142dc8011d1b7a00de83e4aab38ce4" + // Test keys (from here https://github.com/jagerman/session-pysogs/blob/docs/contrib/auth-example.py) + static let publicKey: String = "88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b" + static let privateKey: String = "30d796c1ddb4dc455fd998a98aa275c247494a9a7bde9c1fee86ae45cd585241" + static let edKeySeed: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9" + static let edPublicKey: String = "bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc" + static let edSecretKey: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc" + static let blindedPublicKey: String = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5" + static let serverPublicKey: String = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d" }