From bf0b0403bc7b0eb55e0479112e8f1119caf9e800 Mon Sep 17 00:00:00 2001 From: Michelle Linington Date: Thu, 20 May 2021 01:22:25 -0700 Subject: [PATCH] Support for SealedSender SenderKeys --- .../src/SMKSecretSessionCipher.swift | 58 +++++++- .../src/SMKSecretSessionCipherTest.swift | 131 +++++++++++++++++- SignalMetadataKitTests/src/SMKTestUtils.swift | 12 +- 3 files changed, 191 insertions(+), 10 deletions(-) diff --git a/SignalMetadataKit/src/SMKSecretSessionCipher.swift b/SignalMetadataKit/src/SMKSecretSessionCipher.swift index a288210..60309dd 100644 --- a/SignalMetadataKit/src/SMKSecretSessionCipher.swift +++ b/SignalMetadataKit/src/SMKSecretSessionCipher.swift @@ -20,11 +20,15 @@ public class SecretSessionKnownSenderError: NSObject, CustomNSError { public let senderAddress: SMKAddress public let senderDeviceId: UInt32 + public let groupId: Data? + public let contentHint: UnidentifiedSenderMessageContent.ContentHint public let underlyingError: Error - init(senderAddress: SMKAddress, senderDeviceId: UInt32, underlyingError: Error) { - self.senderAddress = senderAddress - self.senderDeviceId = senderDeviceId + init(messageContent: UnidentifiedSenderMessageContent, underlyingError: Error) { + self.senderAddress = SMKAddress(messageContent.senderCertificate.sender) + self.senderDeviceId = messageContent.senderCertificate.sender.deviceId + self.groupId = messageContent.groupId.map { Data($0) } + self.contentHint = messageContent.contentHint self.underlyingError = underlyingError } @@ -97,6 +101,7 @@ private class SMKStaticKeys: NSObject { @objc public enum SMKMessageType: Int { case whisper case prekey + case senderKey } @objc @@ -152,6 +157,8 @@ fileprivate extension SMKMessageType { self = .whisper case .preKey: self = .prekey + case .senderKey: + self = .senderKey default: fatalError("not ready for other kinds of messages yet") } @@ -168,17 +175,20 @@ fileprivate extension SMKMessageType { private let preKeyStore: PreKeyStore private let signedPreKeyStore: SignedPreKeyStore private let identityStore: IdentityKeyStore + private let senderKeyStore: SenderKeyStore // public SecretSessionCipher(SignalProtocolStore signalProtocolStore) { public init(sessionStore: SessionStore, preKeyStore: PreKeyStore, signedPreKeyStore: SignedPreKeyStore, - identityStore: IdentityKeyStore) throws { + identityStore: IdentityKeyStore, + senderKeyStore: SenderKeyStore) throws { self.sessionStore = sessionStore self.preKeyStore = preKeyStore self.signedPreKeyStore = signedPreKeyStore self.identityStore = identityStore + self.senderKeyStore = senderKeyStore } // MARK: - Public @@ -204,6 +214,37 @@ fileprivate extension SMKMessageType { context: protocolContext ?? NullContext())) } + public func throwswrapped_groupEncryptMessage(recipients: [ProtocolAddress], + paddedPlaintext: Data, + senderCertificate: SenderCertificate, + groupId: Data, + distributionId: UUID, + contentHint: UnidentifiedSenderMessageContent.ContentHint = .default, + protocolContext: StoreContext?) throws -> Data { + + let senderAddress = try ProtocolAddress(from: senderCertificate.sender) + let ciphertext = try groupEncrypt( + paddedPlaintext, + from: senderAddress, + distributionId: distributionId, + store: senderKeyStore, + context: protocolContext ?? NullContext()) + + let udMessageContent = try UnidentifiedSenderMessageContent( + ciphertext, + from: senderCertificate, + contentHint: contentHint, + groupId: groupId) + + let multiRecipientMessage = try sealedSenderMultiRecipientEncrypt( + udMessageContent, + for: recipients, + identityStore: identityStore, + context: protocolContext ?? NullContext()) + + return Data(multiRecipientMessage) + } + // public Pair decrypt(CertificateValidator validator, byte[] ciphertext, long timestamp) // throws InvalidMetadataMessageException, InvalidMetadataVersionException, ProtocolInvalidMessageException, ProtocolInvalidKeyException, ProtocolNoSessionException, ProtocolLegacyMessageException, ProtocolInvalidVersionException, ProtocolDuplicateMessageException, ProtocolInvalidKeyIdException, ProtocolUntrustedIdentityException public func throwswrapped_decryptMessage(certificateValidator: SMKCertificateValidator, @@ -254,8 +295,7 @@ fileprivate extension SMKMessageType { paddedPayload: Data(paddedMessagePlaintext), messageType: SMKMessageType(messageContent.messageType)) } catch { - throw SecretSessionKnownSenderError(senderAddress: SMKAddress(senderAddress), - senderDeviceId: senderAddress.deviceId, + throw SecretSessionKnownSenderError(messageContent: messageContent, underlyingError: error) } } @@ -303,6 +343,12 @@ fileprivate extension SMKMessageType { preKeyStore: preKeyStore, signedPreKeyStore: signedPreKeyStore, context: context) + case .senderKey: + plaintextData = try groupDecrypt( + messageContent.contents, + from: ProtocolAddress(from: sender), + store: senderKeyStore, + context: context) case let unknownType: throw SMKError.assertionError( description: "\(logTag) Not prepared to handle this message type: \(unknownType.rawValue)") diff --git a/SignalMetadataKitTests/src/SMKSecretSessionCipherTest.swift b/SignalMetadataKitTests/src/SMKSecretSessionCipherTest.swift index 8b931d1..80037a7 100644 --- a/SignalMetadataKitTests/src/SMKSecretSessionCipherTest.swift +++ b/SignalMetadataKitTests/src/SMKSecretSessionCipherTest.swift @@ -4,7 +4,7 @@ import XCTest import SignalMetadataKit -import SignalClient +@testable import SignalClient import Curve25519Kit // https://github.com/signalapp/libsignal-metadata-java/blob/master/tests/src/test/java/org/signal/libsignal/metadata/SecretSessionCipherTest.java @@ -252,6 +252,135 @@ class SMKSecretSessionCipherTest: XCTestCase { } } + func testGroupEncryptDecrypt_Success() { + // Setup: Initialize sessions and sender certificate + let aliceMockClient = MockClient(address: aliceAddress, deviceId: 1, registrationId: 1234) + let bobMockClient = MockClient(address: bobAddress, deviceId: 1, registrationId: 1235) + initializeSessions(aliceMockClient: aliceMockClient, bobMockClient: bobMockClient) + + let trustRoot = IdentityKeyPair.generate() + let senderCertificate = createCertificateFor( + trustRoot: trustRoot, + senderAddress: aliceMockClient.address, + senderDeviceId: UInt32(aliceMockClient.deviceId), + identityKey: aliceMockClient.identityKeyPair.publicKey, + expirationTimestamp: 31337) + + // Setup: Distribute alice's sender key to bob's key store + let distributionId = UUID() + let aliceSenderKeyMessage = try! SenderKeyDistributionMessage( + from: aliceMockClient.protocolAddress, + distributionId: distributionId, + store: aliceMockClient.senderKeyStore, + context: NullContext()) + + try! processSenderKeyDistributionMessage( + aliceSenderKeyMessage, + from: aliceMockClient.protocolAddress, + store: bobMockClient.senderKeyStore, + context: NullContext()) + + // Test: Alice encrypt's a message using `groupEncryptMessage` + let aliceCipher = try! aliceMockClient.createSecretSessionCipher() + let alicePlaintext = "beltalowda".data(using: String.Encoding.utf8)! + let aliceCiphertext = try! aliceCipher.throwswrapped_groupEncryptMessage( + recipients: [bobMockClient.protocolAddress], + paddedPlaintext: alicePlaintext, + senderCertificate: senderCertificate, + groupId: Data(), + distributionId: distributionId, + contentHint: .retry, + protocolContext: nil).map { $0 } + + // This splits out irrelevant per-recipient data from the shared sender key message + // This is only necessary in tests. The server would usually handle this. + let singleRecipientCiphertext = try! sealedSenderMultiRecipientMessageForSingleRecipient(aliceCiphertext) + + // Test: Bob decrypts the ciphertext + let bobCipher = try! bobMockClient.createSecretSessionCipher() + let bobValidator = SMKCertificateDefaultValidator(trustRoot: ECPublicKey(trustRoot.publicKey)) + let bobPlaintext = try! bobCipher.throwswrapped_decryptMessage( + certificateValidator: bobValidator, + cipherTextData: Data(singleRecipientCiphertext), + timestamp: 31335, + localE164: bobMockClient.recipientE164, + localUuid: bobMockClient.recipientUuid, + localDeviceId: bobMockClient.deviceId, + protocolContext: nil) + + // Verify + XCTAssertEqual(String(data: bobPlaintext.paddedPayload, encoding: .utf8), "beltalowda") + XCTAssertEqual(bobPlaintext.senderAddress, aliceMockClient.address) + XCTAssertEqual(bobPlaintext.senderDeviceId, Int(aliceMockClient.deviceId)) + XCTAssertEqual(bobPlaintext.messageType, .senderKey) + } + + func testGroupEncryptDecrypt_Failure() { + // Setup: Initialize sessions and sender certificate + let aliceMockClient = MockClient(address: aliceAddress, deviceId: 1, registrationId: 1234) + let bobMockClient = MockClient(address: bobAddress, deviceId: 1, registrationId: 1235) + initializeSessions(aliceMockClient: aliceMockClient, bobMockClient: bobMockClient) + + let trustRoot = IdentityKeyPair.generate() + let senderCertificate = createCertificateFor( + trustRoot: trustRoot, + senderAddress: aliceMockClient.address, + senderDeviceId: UInt32(aliceMockClient.deviceId), + identityKey: aliceMockClient.identityKeyPair.publicKey, + expirationTimestamp: 31337) + + // Setup: Alice creates a sender key + // Test: Bob intentionally does not process Alice's SKDM to simulate an unsent key + let distributionId = UUID() + let _ = try! SenderKeyDistributionMessage( + from: aliceMockClient.protocolAddress, + distributionId: distributionId, + store: aliceMockClient.senderKeyStore, + context: NullContext()) + + // Test: Alice encrypt's a message using `groupEncryptMessage` + let aliceCipher = try! aliceMockClient.createSecretSessionCipher() + let alicePlaintext = "beltalowda".data(using: String.Encoding.utf8)! + let aliceCiphertext = try! aliceCipher.throwswrapped_groupEncryptMessage( + recipients: [bobMockClient.protocolAddress], + paddedPlaintext: alicePlaintext, + senderCertificate: senderCertificate, + groupId: "inyalowda".data(using: String.Encoding.utf8)!, + distributionId: distributionId, + contentHint: .retry, + protocolContext: nil).map { $0 } + + // This splits out irrelevant per-recipient data from the shared sender key message + // This is only necessary in tests. The server would usually handle this. + let singleRecipientCiphertext = try! sealedSenderMultiRecipientMessageForSingleRecipient(aliceCiphertext) + + // Test: Bob decrypts the ciphertext + let bobCipher = try! bobMockClient.createSecretSessionCipher() + let bobValidator = SMKCertificateDefaultValidator(trustRoot: ECPublicKey(trustRoot.publicKey)) + do { + _ = try bobCipher.throwswrapped_decryptMessage( + certificateValidator: bobValidator, + cipherTextData: Data(singleRecipientCiphertext), + timestamp: 31335, + localE164: bobMockClient.recipientE164, + localUuid: bobMockClient.recipientUuid, + localDeviceId: bobMockClient.deviceId, + protocolContext: nil) + XCTFail("Decryption should have failed.") + } catch let knownSenderError as SecretSessionKnownSenderError { + XCTAssertEqual(knownSenderError.senderAddress, aliceMockClient.address) + XCTAssertEqual(knownSenderError.senderDeviceId, UInt32(aliceMockClient.deviceId)) + XCTAssertEqual(Data(knownSenderError.groupId!), "inyalowda".data(using: String.Encoding.utf8)!) + if case SignalError.invalidState(_) = knownSenderError.underlyingError { + // Expected + } else { + XCTFail() + } + } catch { + XCTFail("Unexpected error: \(error)") + } + } + // MARK: - Utils // private SenderCertificate createCertificateFor(ECKeyPair trustRoot, String sender, int deviceId, ECPublicKey identityKey, long expires) diff --git a/SignalMetadataKitTests/src/SMKTestUtils.swift b/SignalMetadataKitTests/src/SMKTestUtils.swift index e6a37e2..5d486ae 100644 --- a/SignalMetadataKitTests/src/SMKTestUtils.swift +++ b/SignalMetadataKitTests/src/SMKTestUtils.swift @@ -32,6 +32,9 @@ class MockClient: NSObject { } let address: SMKAddress + var protocolAddress: ProtocolAddress { + try! ProtocolAddress(name: address.uuid!.uuidString, deviceId: UInt32(deviceId)) + } let deviceId: Int32 let registrationId: Int32 @@ -42,6 +45,7 @@ class MockClient: NSObject { let preKeyStore: InMemorySignalProtocolStore let signedPreKeyStore: InMemorySignalProtocolStore let identityStore: InMemorySignalProtocolStore + let senderKeyStore: InMemorySignalProtocolStore init(address: SMKAddress, deviceId: Int32, registrationId: Int32) { self.address = address @@ -56,13 +60,15 @@ class MockClient: NSObject { preKeyStore = protocolStore signedPreKeyStore = protocolStore identityStore = protocolStore + senderKeyStore = protocolStore } func createSecretSessionCipher() throws -> SMKSecretSessionCipher { return try SMKSecretSessionCipher(sessionStore: sessionStore, - preKeyStore: preKeyStore, - signedPreKeyStore: signedPreKeyStore, - identityStore: identityStore) + preKeyStore: preKeyStore, + signedPreKeyStore: signedPreKeyStore, + identityStore: identityStore, + senderKeyStore: senderKeyStore) } func generateMockPreKey() -> PreKeyRecord {