Support for SealedSender SenderKeys
This commit is contained in:
parent
af0c4d1753
commit
bf0b0403bc
@ -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<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)")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user