Merge branch 'mlin/PR/SenderKey' into feature/SignalClient-adoption

This commit is contained in:
Michelle Linington 2021-05-20 17:10:25 -07:00
commit 5c822fb4d0
3 changed files with 196 additions and 10 deletions

View File

@ -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 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<SignalProtocolAddress, byte[]> 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)")

View File

@ -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,140 @@ 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.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.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 {
// Verify: We need to make sure that the sender, group, and contentHint are preserved
// through decryption failures because of missing a missing sender key. This will
// help with recovery.
XCTAssertEqual(knownSenderError.senderAddress, aliceMockClient.address)
XCTAssertEqual(knownSenderError.senderDeviceId, UInt32(aliceMockClient.deviceId))
XCTAssertEqual(Data(knownSenderError.groupId!), "inyalowda".data(using: String.Encoding.utf8)!)
XCTAssertEqual(knownSenderError.contentHint, .retry)
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)

View File

@ -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 {