// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import Sodium import GRDB import SessionUtilitiesKit import Quick import Nimble @testable import SessionMessagingKit class MessageReceiverDecryptionSpec: QuickSpec { override class func spec() { // MARK: Configuration @TestState var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, SNMessagingKit.self ], initialData: { db in try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } ) @TestState var mockCrypto: MockCrypto! = MockCrypto( initialSetup: { crypto in crypto .when { crypto in try crypto.perform( .encryptAeadXChaCha20( message: anyArray(), secretKey: anyArray(), nonce: anyArray(), using: any() ) ) } .thenReturn(nil) crypto .when { try $0.perform( .open( anonymousCipherText: anyArray(), recipientPublicKey: anyArray(), recipientSecretKey: anyArray() ) ) } .thenReturn([UInt8](repeating: 0, count: 100)) crypto .when { crypto in crypto.generate( .blindedKeyPair( serverPublicKey: any(), edKeyPair: any(), using: any() ) ) } .thenReturn( KeyPair( publicKey: Data(hex: TestConstants.blindedPublicKey).bytes, secretKey: Data(hex: TestConstants.edSecretKey).bytes ) ) crypto .when { crypto in try crypto.perform( .sharedBlindedEncryptionKey( secretKey: anyArray(), otherBlindedPublicKey: anyArray(), fromBlindedPublicKey: anyArray(), toBlindedPublicKey: anyArray(), using: any() ) ) } .thenReturn([]) crypto .when { crypto in try crypto.perform( .generateBlindingFactor(serverPublicKey: any(), using: any()) ) } .thenReturn([]) crypto .when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) } .thenReturn(Data(hex: TestConstants.blindedPublicKey).bytes) crypto .when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) } .thenReturn(Data(hex: TestConstants.publicKey).bytes) crypto .when { $0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray())) } .thenReturn(true) crypto .when { try $0.perform( .decryptAeadXChaCha20( authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray() ) ) } .thenReturn("TestMessage".data(using: .utf8)!.bytes + [UInt8](repeating: 0, count: 32)) crypto.when { $0.size(.nonce24) }.thenReturn(24) crypto.when { $0.size(.publicKey) }.thenReturn(32) crypto.when { $0.size(.signature) }.thenReturn(64) crypto .when { try $0.perform(.generateNonce24()) } .thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes) } ) @TestState var dependencies: Dependencies! = Dependencies( storage: mockStorage, crypto: mockCrypto ) // MARK: - a MessageReceiver describe("a MessageReceiver") { // MARK: -- when decrypting with the session protocol context("when decrypting with the session protocol") { // MARK: ---- successfully decrypts a message it("successfully decrypts a message") { let result = try? MessageReceiver.decryptWithSessionProtocol( ciphertext: Data( base64Encoded: "SRP0eBUWh4ez6ppWjUs5/Wph5fhnPRgB5zsWWnTz+FBAw/YI3oS2pDpIfyetMTbU" + "sFMhE5G4PbRtQFey1hsxLl221Qivc3ayaX2Mm/X89Dl8e45BC+Lb/KU9EdesxIK4pVgYXs9XrMtX3v8" + "dt0eBaXneOBfr7qB8pHwwMZjtkOu1ED07T9nszgbWabBphUfWXe2U9K3PTRisSCI=" )!, using: KeyPair( publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes ), using: Dependencies() ) expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage")) expect(result?.senderX25519PublicKey) .to(equal("0588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b")) } // MARK: ---- throws an error if it cannot open the message it("throws an error if it cannot open the message") { mockCrypto .when { try $0.perform( .open( anonymousCipherText: anyArray(), recipientPublicKey: anyArray(), recipientSecretKey: anyArray() ) ) } .thenReturn(nil) expect { try MessageReceiver.decryptWithSessionProtocol( ciphertext: "TestMessage".data(using: .utf8)!, using: KeyPair( publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if the open message is too short it("throws an error if the open message is too short") { mockCrypto .when { try $0.perform( .open( anonymousCipherText: anyArray(), recipientPublicKey: anyArray(), recipientSecretKey: anyArray() ) ) } .thenReturn([1, 2, 3]) expect { try MessageReceiver.decryptWithSessionProtocol( ciphertext: "TestMessage".data(using: .utf8)!, using: KeyPair( publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if it cannot verify the message it("throws an error if it cannot verify the message") { mockCrypto .when { $0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray())) } .thenReturn(false) expect { try MessageReceiver.decryptWithSessionProtocol( ciphertext: "TestMessage".data(using: .utf8)!, using: KeyPair( publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.invalidSignature)) } // MARK: ---- throws an error if it cannot get the senders x25519 public key it("throws an error if it cannot get the senders x25519 public key") { mockCrypto.when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) }.thenReturn(nil) expect { try MessageReceiver.decryptWithSessionProtocol( ciphertext: "TestMessage".data(using: .utf8)!, using: KeyPair( publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } } // MARK: -- when decrypting with the blinded session protocol context("when decrypting with the blinded session protocol") { // MARK: ---- successfully decrypts a message 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: 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")) } // MARK: ---- successfully decrypts a mocked incoming message 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: 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")) } // MARK: ---- throws an error if the data is too short 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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if it cannot get the blinded keyPair it("throws an error if it cannot get the blinded keyPair") { mockCrypto .when { [dependencies = dependencies!] crypto in crypto.generate( .blindedKeyPair( serverPublicKey: any(), edKeyPair: any(), using: dependencies ) ) } .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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if it cannot get the decryption key it("throws an error if it cannot get the decryption key") { mockCrypto .when { [dependencies = dependencies!] crypto in try crypto.perform( .sharedBlindedEncryptionKey( secretKey: anyArray(), otherBlindedPublicKey: anyArray(), fromBlindedPublicKey: anyArray(), toBlindedPublicKey: anyArray(), using: dependencies ) ) } .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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if the data version is not 0 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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if it cannot decrypt the data it("throws an error if it cannot decrypt the data") { mockCrypto .when { try $0.perform( .decryptAeadXChaCha20( 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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if the inner bytes are too short it("throws an error if the inner bytes are too short") { mockCrypto .when { try $0.perform( .decryptAeadXChaCha20( 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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } // MARK: ---- throws an error if it cannot generate the blinding factor it("throws an error if it cannot generate the blinding factor") { mockCrypto .when { [dependencies = dependencies!] crypto in try crypto.perform(.generateBlindingFactor(serverPublicKey: any(), using: dependencies)) } .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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.invalidSignature)) } // MARK: ---- throws an error if it cannot generate the combined key it("throws an error if it cannot generate the combined key") { mockCrypto .when { try $0.perform(.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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.invalidSignature)) } // MARK: ---- throws an error if the combined key does not match kA it("throws an error if the combined key does not match kA") { mockCrypto .when { try $0.perform(.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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.invalidSignature)) } // MARK: ---- throws an error if it cannot get the senders x25519 public key it("throws an error if it cannot get the senders x25519 public key") { mockCrypto .when { try $0.perform(.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: KeyPair( publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes, secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes ), using: dependencies ) } .to(throwError(MessageReceiverError.decryptionFailed)) } } } } }