Compare commits

..

17 Commits

Author SHA1 Message Date
Michelle Linington
90f1776e63 Merge branch 'mlin/PR/ExposeUnsealedContent' 2021-09-17 13:12:55 -07:00
Michelle Linington
2ca9aafb27 Expose unsealed ciphertext content 2021-09-16 22:04:01 -07:00
Matthew Chen
998b761970 Fix build break. 2021-09-08 18:01:38 -03:00
Michelle Linington
4c8ddf0b04 Merge branch 'mlin/PR/ExposeGroupIdForSingleRecipientMessages' into feature/SignalClient-adoption 2021-08-12 19:45:14 -07:00
Michelle Linington
d8afca956d Callers can now add groupId to single recipient messages
We need to plumb through groupId. Desktop needs this to properly handle
resend responses.
2021-08-10 21:24:08 -07:00
Michelle Linington
7e59c7e818 Merge branch 'mlin/PR/ZeroedAccessKey' into feature/SignalClient-adoption 2021-08-04 17:08:53 -07:00
Michelle Linington
dcf17cdade Add zeroed SMKUDAccessKey
Used against the multi_recipient endpoint. The server doesn't need an
access key for unrestricted UD recipients.
2021-08-04 17:07:01 -07:00
Michelle Linington
882049b3f2 Adopt renames from SignalClient 2021-07-21 23:08:10 -07:00
Michelle Linington
c5f427f1bc Merge branch 'mlin/PR/LeftoverSenderKeyWork' into feature/SignalClient-adoption 2021-07-21 23:07:21 -07:00
Michelle Linington
242caf0d54 Plumbing of cipher type through decryption errors 2021-07-20 17:54:39 -07:00
Michelle Linington
06d7766c0e Support for decrypting PlaintextContent type
Also, returns the contentHint and groupId of any sealed-sender
decryption errors.
2021-06-02 21:19:12 -07:00
Michelle Linington
d1a3d73c5f Composable access keys
Sender key requires a derived access key that's composed of each
recipients access key xor-ed.
2021-05-28 00:33:53 -07:00
Michelle Linington
5c822fb4d0 Merge branch 'mlin/PR/SenderKey' into feature/SignalClient-adoption 2021-05-20 17:10:25 -07:00
Michelle Linington
6ac7b020fc PR Feedback and minor test change
Originally I used the `throwswrapped` prefix to match existing naming
convention. Jordan said this is a legacy convention.
2021-05-20 17:08:36 -07:00
Michelle Linington
bf0b0403bc Support for SealedSender SenderKeys 2021-05-20 01:22:25 -07:00
Jordan Rose
af0c4d1753 Merge branch 'jrose/remove-AxolotlKit' into feature/SignalClient-adoption 2021-03-22 16:09:30 -07:00
Jordan Rose
564fa5f1f7 Remove last references to AxolotlKit (and HKDFKit) 2021-03-19 18:27:32 -07:00
7 changed files with 374 additions and 129 deletions

View File

@ -39,15 +39,12 @@ A Swift & Objective-C library used by other Signal libraries.
s.resources = ["SignalMetadataKit/Resources/Certificates/*"]
s.dependency 'AxolotlKit'
s.dependency 'CocoaLumberjack'
s.dependency 'Curve25519Kit'
s.dependency 'HKDFKit'
s.dependency 'SignalClient'
s.dependency 'SignalCoreKit'
s.dependency 'SwiftProtobuf'
s.dependency 'SignalClient'
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'SignalMetadataKitTests/src/**/*.{h,m,swift}'
end

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
@ -11,10 +11,7 @@ public enum SMKCertificateError: Error {
case invalidCertificate(description: String)
}
@objc(SMKCertificateValidator)
public protocol SMKCertificateValidatorObjC {}
public protocol SMKCertificateValidator: SMKCertificateValidatorObjC {
public protocol SMKCertificateValidator {
func throwswrapped_validate(senderCertificate: SenderCertificate, validationTime: UInt64) throws
func throwswrapped_validate(serverCertificate: ServerCertificate) throws
}
@ -53,9 +50,9 @@ public protocol SMKCertificateValidator: SMKCertificateValidatorObjC {
// }
guard try serverCertificate.publicKey.verifySignature(message: senderCertificate.certificateBytes,
signature: senderCertificate.signatureBytes) else {
OWSLog.error("Sender certificate signature verification failed.")
Logger.error("Sender certificate signature verification failed.")
let error = SMKCertificateError.invalidCertificate(description: "Sender certificate signature verification failed.")
OWSLog.error("\(error)")
Logger.error("\(error)")
throw error
}
@ -64,7 +61,7 @@ public protocol SMKCertificateValidator: SMKCertificateValidatorObjC {
// }
guard validationTime <= senderCertificate.expiration else {
let error = SMKCertificateError.invalidCertificate(description: "Certficate is expired.")
OWSLog.error("\(error)")
Logger.error("\(error)")
throw error
}
@ -82,7 +79,7 @@ public protocol SMKCertificateValidator: SMKCertificateValidatorObjC {
guard try trustRoot.key.verifySignature(message: serverCertificate.certificateBytes,
signature: serverCertificate.signatureBytes) else {
let error = SMKCertificateError.invalidCertificate(description: "Server certificate signature verification failed.")
OWSLog.error("\(error)")
Logger.error("\(error)")
throw error
}
@ -91,7 +88,7 @@ public protocol SMKCertificateValidator: SMKCertificateValidatorObjC {
// }
guard !SMKCertificateDefaultValidator.kRevokedCertificateIds.contains(serverCertificate.keyId) else {
let error = SMKCertificateError.invalidCertificate(description: "Revoked certificate.")
OWSLog.error("\(error)")
Logger.error("\(error)")
throw error
}

View File

@ -3,47 +3,28 @@
//
import Foundation
import AxolotlKit
import Curve25519Kit
import SignalCoreKit
import SignalClient
@objc
public class SecretSessionKnownSenderError: NSObject, CustomNSError {
@objc
public static let kSenderE164Key = "kSenderE164Key"
@objc
public static let kSenderUuidKey = "kSenderUuidKey"
@objc
public static let kSenderDeviceIdKey = "kSenderDeviceIdKey"
public struct SecretSessionKnownSenderError: Error {
public let senderAddress: SMKAddress
public let senderDeviceId: UInt32
public let cipherType: CiphertextMessage.MessageType
public let groupId: Data?
public let unsealedContent: 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.cipherType = messageContent.messageType
self.groupId = messageContent.groupId.map { Data($0) }
self.unsealedContent = Data(messageContent.contents)
self.contentHint = messageContent.contentHint
self.underlyingError = underlyingError
}
public var errorUserInfo: [String: Any] {
var info: [String: Any] = [
type(of: self).kSenderDeviceIdKey: self.senderDeviceId,
NSUnderlyingErrorKey: (underlyingError as NSError)
]
if let e164 = senderAddress.e164 {
info[type(of: self).kSenderE164Key] = e164
}
if let uuid = senderAddress.uuid {
info[type(of: self).kSenderUuidKey] = uuid
}
return info
}
}
@objc
@ -97,6 +78,8 @@ private class SMKStaticKeys: NSObject {
@objc public enum SMKMessageType: Int {
case whisper
case prekey
case senderKey
case plaintext
}
@objc
@ -152,6 +135,10 @@ fileprivate extension SMKMessageType {
self = .whisper
case .preKey:
self = .prekey
case .senderKey:
self = .senderKey
case .plaintext:
self = .plaintext
default:
fatalError("not ready for other kinds of messages yet")
}
@ -164,74 +151,113 @@ fileprivate extension SMKMessageType {
private let kSMKSecretSessionCipherMacLength: UInt = 10
private let sessionStore: SignalClient.SessionStore
private let preKeyStore: SignalClient.PreKeyStore
private let signedPreKeyStore: SignalClient.SignedPreKeyStore
private let identityStore: SignalClient.IdentityKeyStore
private let sessionStore: SessionStore
private let preKeyStore: PreKeyStore
private let signedPreKeyStore: SignedPreKeyStore
private let identityStore: IdentityKeyStore
private let senderKeyStore: SenderKeyStore
// public SecretSessionCipher(SignalProtocolStore signalProtocolStore) {
public init(sessionStore: SignalClient.SessionStore,
preKeyStore: SignalClient.PreKeyStore,
signedPreKeyStore: SignalClient.SignedPreKeyStore,
identityStore: SignalClient.IdentityKeyStore) throws {
public init(sessionStore: SessionStore,
preKeyStore: PreKeyStore,
signedPreKeyStore: SignedPreKeyStore,
identityStore: IdentityKeyStore,
senderKeyStore: SenderKeyStore) throws {
self.sessionStore = sessionStore
self.preKeyStore = preKeyStore
self.signedPreKeyStore = signedPreKeyStore
self.identityStore = identityStore
}
@objc(initWithSessionStore:preKeyStore:signedPreKeyStore:identityStore:error:)
public convenience init(transitionalSessionStore sessionStore: AxolotlKit.SessionStore,
preKeyStore: AxolotlKit.PreKeyStore,
signedPreKeyStore: AxolotlKit.SignedPreKeyStore,
identityStore: AxolotlKit.IdentityKeyStore) throws {
try self.init(
sessionStore: sessionStore as! SignalClient.SessionStore,
preKeyStore: preKeyStore as! SignalClient.PreKeyStore,
signedPreKeyStore: signedPreKeyStore as! SignalClient.SignedPreKeyStore,
identityStore: identityStore as! SignalClient.IdentityKeyStore)
self.senderKeyStore = senderKeyStore
}
// MARK: - Public
// public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext)
public func throwswrapped_encryptMessage(recipient: SMKAddress,
deviceId: Int32,
paddedPlaintext: Data,
senderCertificate: SenderCertificate,
protocolContext: SPKProtocolWriteContext?) throws -> Data {
public func encryptMessage(
recipient: SMKAddress,
deviceId: Int32,
paddedPlaintext: Data,
contentHint: UnidentifiedSenderMessageContent.ContentHint,
groupId: Data?,
senderCertificate: SenderCertificate,
protocolContext: StoreContext
) throws -> Data {
guard deviceId > 0 else {
throw SMKError.assertionError(description: "\(logTag) invalid deviceId")
}
// CiphertextMessage message = new SessionCipher(signalProtocolStore, destinationAddress).encrypt(paddedPlaintext);
let recipientAddress = try ProtocolAddress(from: recipient, deviceId: UInt32(bitPattern: deviceId))
// Allow nil contexts for testing.
return Data(try sealedSenderEncrypt(message: paddedPlaintext,
for: recipientAddress,
from: senderCertificate,
sessionStore: sessionStore,
identityStore: identityStore,
context: (protocolContext as! StoreContext?) ?? NullContext()))
let ciphertextMessage = try signalEncrypt(
message: paddedPlaintext,
for: recipientAddress,
sessionStore: sessionStore,
identityStore: identityStore,
context: protocolContext)
let usmc = try UnidentifiedSenderMessageContent(
ciphertextMessage,
from: senderCertificate,
contentHint: contentHint,
groupId: groupId ?? Data())
let outerBytes = try sealedSenderEncrypt(
usmc,
for: recipientAddress,
identityStore: identityStore,
context: protocolContext)
return Data(outerBytes)
}
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,
sessionStore: sessionStore,
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
@objc
public func throwswrapped_decryptMessage(certificateValidator: SMKCertificateValidatorObjC,
public func throwswrapped_decryptMessage(certificateValidator: SMKCertificateValidator,
cipherTextData: Data,
timestamp: UInt64,
localE164: String?,
localUuid: UUID?,
localDeviceId: Int32,
protocolContext: SPKProtocolWriteContext?) throws -> SMKDecryptResult {
protocolContext: StoreContext?) throws -> SMKDecryptResult {
guard timestamp > 0 else {
throw SMKError.assertionError(description: "\(logTag) invalid timestamp")
}
// Allow nil contexts for testing.
let context = (protocolContext as! StoreContext?) ?? NullContext()
let context = protocolContext ?? NullContext()
let messageContent = try UnidentifiedSenderMessageContent(message: cipherTextData,
identityStore: self.identityStore,
context: context)
@ -241,13 +267,12 @@ fileprivate extension SMKMessageType {
guard !SMKAddress(senderAddress).matches(localAddress) ||
Int32(bitPattern: senderAddress.deviceId) != localDeviceId else {
OWSLog.info("Discarding self-sent message")
Logger.info("Discarding self-sent message")
throw SMKSecretSessionCipherError.selfSentMessage
}
do {
// validator.validate(content.getSenderCertificate(), timestamp);
let certificateValidator = certificateValidator as! SMKCertificateValidator
try certificateValidator.throwswrapped_validate(
senderCertificate: messageContent.senderCertificate,
validationTime: timestamp)
@ -268,8 +293,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)
}
}
@ -317,6 +341,15 @@ 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 .plaintext:
let plaintextMessage = try PlaintextContent(bytes: messageContent.contents)
plaintextData = plaintextMessage.body
case let unknownType:
throw SMKError.assertionError(
description: "\(logTag) Not prepared to handle this message type: \(unknownType.rawValue)")
@ -324,3 +357,28 @@ fileprivate extension SMKMessageType {
return Data(plaintextData)
}
}
// MARK: - Internal for testing
extension SMKSecretSessionCipher {
// Only allow nil contexts for testing
func encryptMessage(
recipient: SMKAddress,
deviceId: Int32,
paddedPlaintext: Data,
contentHint: UnidentifiedSenderMessageContent.ContentHint = .default,
groupId: Data? = nil,
senderCertificate: SenderCertificate,
protocolContext: StoreContext? = nil
) throws -> Data {
try encryptMessage(
recipient: recipient,
deviceId: deviceId,
paddedPlaintext: paddedPlaintext,
contentHint: contentHint,
groupId: groupId,
senderCertificate: senderCertificate,
protocolContext: protocolContext ?? NullContext())
}
}

View File

@ -42,10 +42,30 @@ public class SMKUDAccessKey: NSObject {
self.keyData = Randomness.generateRandomBytes(Int32(SMKUDAccessKey.kUDAccessKeyLength))
}
private init(keyData: Data) {
self.keyData = keyData
}
/// Used to compose multiple Unidentified-Access-Keys for the multiRecipient endpoint
public static func ^(lhs: SMKUDAccessKey, rhs: SMKUDAccessKey) -> SMKUDAccessKey {
owsAssert(lhs.keyData.count == SMKUDAccessKey.kUDAccessKeyLength)
owsAssert(rhs.keyData.count == SMKUDAccessKey.kUDAccessKeyLength)
let xoredBytes = zip(lhs.keyData, rhs.keyData).map(^)
return .init(keyData: Data(xoredBytes))
}
// MARK:
override public func isEqual(_ object: Any?) -> Bool {
guard let other = object as? SMKUDAccessKey else { return false }
return self.keyData == other.keyData
}
// Unrestricted UD recipients should have a zeroed access key sent to the multi-recipient endpoint
// For a collection of mixed recipients, a zeroed key will have no effect composing keys with xor
// For a collection of only unrestricted UD recipients, the server expects a zero access key
public static var zeroedKey: SMKUDAccessKey {
.init(keyData: Data(repeating: 0, count: kUDAccessKeyLength))
}
}

View File

@ -3,7 +3,7 @@
//
import XCTest
import SignalMetadataKit
@testable import SignalMetadataKit
import SignalCoreKit
import Curve25519Kit
import SignalClient
@ -51,11 +51,10 @@ class SMKTest: XCTestCase {
let plaintext = Randomness.generateRandomBytes(200)
let paddedPlaintext = (plaintext as NSData).paddedMessageBody()
let senderCertificate = try! SenderCertificate(buildSenderCertificateProto(senderClient: aliceMockClient).serializedData())
let encryptedMessage = try! aliceToBobCipher.throwswrapped_encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: paddedPlaintext,
senderCertificate: senderCertificate,
protocolContext: nil)
let encryptedMessage = try! aliceToBobCipher.encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: paddedPlaintext,
senderCertificate: senderCertificate)
let messageTimestamp = NSDate.ows_millisecondTimeStamp()

View File

@ -3,9 +3,10 @@
//
import XCTest
import SignalMetadataKit
import SignalClient
@testable import SignalMetadataKit
@testable import SignalClient
import Curve25519Kit
import SignalCoreKit
// https://github.com/signalapp/libsignal-metadata-java/blob/master/tests/src/test/java/org/signal/libsignal/metadata/SecretSessionCipherTest.java
// public class SecretSessionCipherTest extends TestCase {
@ -39,11 +40,10 @@ class SMKSecretSessionCipherTest: XCTestCase {
// senderCertificate, "smert za smert".getBytes());
// NOTE: The java tests don't bother padding the plaintext.
let alicePlaintext = "smert za smert".data(using: String.Encoding.utf8)!
let ciphertext = try! aliceCipher.throwswrapped_encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
senderCertificate: senderCertificate,
protocolContext: nil)
let ciphertext = try! aliceCipher.encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
senderCertificate: senderCertificate)
// SealedSessionCipher bobCipher = new SealedSessionCipher(bobStore, new SignalProtocolAddress("+14152222222", 1));
let bobCipher: SMKSecretSessionCipher = try! bobMockClient.createSecretSessionCipher()
@ -95,11 +95,14 @@ class SMKSecretSessionCipherTest: XCTestCase {
// senderCertificate, "и вот я".getBytes());
// NOTE: The java tests don't bother padding the plaintext.
let alicePlaintext = "и вот я".data(using: String.Encoding.utf8)!
let ciphertext = try! aliceCipher.throwswrapped_encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
senderCertificate: senderCertificate,
protocolContext: nil)
let aliceGroupId = Randomness.generateRandomBytes(6)
let aliceContentHint = UnidentifiedSenderMessageContent.ContentHint.implicit
let ciphertext = try! aliceCipher.encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
contentHint: aliceContentHint,
groupId: aliceGroupId,
senderCertificate: senderCertificate)
// SecretSessionCipher bobCipher = new SecretSessionCipher(bobStore);
let bobCipher: SMKSecretSessionCipher = try! bobMockClient.createSecretSessionCipher()
@ -123,6 +126,16 @@ class SMKSecretSessionCipherTest: XCTestCase {
} catch let knownSenderError as SecretSessionKnownSenderError {
// Decryption is expected to fail.
XCTAssert(knownSenderError.underlyingError is SMKCertificateError )
XCTAssertEqual(knownSenderError.contentHint, aliceContentHint)
XCTAssertEqual(knownSenderError.groupId, aliceGroupId)
XCTAssertNoThrow(
try DecryptionErrorMessage(
originalMessageBytes: knownSenderError.unsealedContent,
type: knownSenderError.cipherType,
timestamp: 31335,
originalSenderDeviceId: knownSenderError.senderDeviceId
)
)
} catch {
XCTFail("Unexpected error: \(error)")
}
@ -156,11 +169,15 @@ class SMKSecretSessionCipherTest: XCTestCase {
// senderCertificate, "и вот я".getBytes());
// NOTE: The java tests don't bother padding the plaintext.
let alicePlaintext = "и вот я".data(using: String.Encoding.utf8)!
let ciphertext = try! aliceCipher.throwswrapped_encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
senderCertificate: senderCertificate,
protocolContext: nil)
let aliceGroupId = Randomness.generateRandomBytes(6)
let aliceContentHint = UnidentifiedSenderMessageContent.ContentHint.resendable
let ciphertext = try! aliceCipher.encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
contentHint: aliceContentHint,
groupId: aliceGroupId,
senderCertificate: senderCertificate)
// SecretSessionCipher bobCipher = new SecretSessionCipher(bobStore);
let bobCipher: SMKSecretSessionCipher = try! bobMockClient.createSecretSessionCipher()
@ -184,6 +201,16 @@ class SMKSecretSessionCipherTest: XCTestCase {
} catch let knownSenderError as SecretSessionKnownSenderError {
// Decryption is expected to fail.
XCTAssert(knownSenderError.underlyingError is SMKCertificateError )
XCTAssertEqual(knownSenderError.contentHint, aliceContentHint)
XCTAssertEqual(knownSenderError.groupId, aliceGroupId)
XCTAssertNoThrow(
try DecryptionErrorMessage(
originalMessageBytes: knownSenderError.unsealedContent,
type: knownSenderError.cipherType,
timestamp: 31338,
originalSenderDeviceId: knownSenderError.senderDeviceId
)
)
} catch {
XCTFail("Unexpected error: \(error)")
}
@ -218,11 +245,10 @@ class SMKSecretSessionCipherTest: XCTestCase {
// senderCertificate, "smert za smert".getBytes());
// NOTE: The java tests don't bother padding the plaintext.
let alicePlaintext = "smert za smert".data(using: String.Encoding.utf8)!
let ciphertext = try! aliceCipher.throwswrapped_encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
senderCertificate: senderCertificate,
protocolContext: nil)
let ciphertext = try! aliceCipher.encryptMessage(recipient: bobMockClient.address,
deviceId: bobMockClient.deviceId,
paddedPlaintext: alicePlaintext,
senderCertificate: senderCertificate)
// SecretSessionCipher bobCipher = new SecretSessionCipher(bobStore);
let bobCipher: SMKSecretSessionCipher = try! bobMockClient.createSecretSessionCipher()
@ -252,6 +278,148 @@ 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: .implicit,
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: .resendable,
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, .resendable)
XCTAssertNoThrow(
try DecryptionErrorMessage(
originalMessageBytes: knownSenderError.unsealedContent,
type: knownSenderError.cipherType,
timestamp: 31335,
originalSenderDeviceId: knownSenderError.senderDeviceId
)
)
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
@ -50,19 +54,21 @@ class MockClient: NSObject {
self.identityKeyPair = IdentityKeyPair.generate()
let protocolStore = InMemorySignalProtocolStore(identity: identityKeyPair,
deviceId: UInt32(bitPattern: deviceId))
registrationId: UInt32(registrationId))
sessionStore = protocolStore
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 {
@ -100,14 +106,14 @@ class MockClient: NSObject {
let bobSignedPreKey = bobMockClient.generateMockSignedPreKey()
// PreKeyBundle bobBundle = new PreKeyBundle(1, 1, 1, bobPreKey.getPublicKey(), 2, bobSignedPreKey.getKeyPair().getPublicKey(), bobSignedPreKey.getSignature(), bobIdentityKey.getPublicKey());
let bobBundle = try! SignalClient.PreKeyBundle(registrationId: UInt32(bitPattern: bobMockClient.registrationId),
deviceId: UInt32(bitPattern: bobMockClient.deviceId),
prekeyId: bobPreKey.id,
prekey: bobPreKey.publicKey,
signedPrekeyId: bobSignedPreKey.id,
signedPrekey: bobSignedPreKey.publicKey,
signedPrekeySignature: bobSignedPreKey.signature,
identity: bobIdentityKey.identityKey)
let bobBundle = try! PreKeyBundle(registrationId: UInt32(bitPattern: bobMockClient.registrationId),
deviceId: UInt32(bitPattern: bobMockClient.deviceId),
prekeyId: bobPreKey.id,
prekey: bobPreKey.publicKey,
signedPrekeyId: bobSignedPreKey.id,
signedPrekey: bobSignedPreKey.publicKey,
signedPrekeySignature: bobSignedPreKey.signature,
identity: bobIdentityKey.identityKey)
// SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, new SignalProtocolAddress("+14152222222", 1));
// aliceSessionBuilder.process(bobBundle);