Compare commits
17 Commits
charlesmch
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f1776e63 | ||
|
|
2ca9aafb27 | ||
|
|
998b761970 | ||
|
|
4c8ddf0b04 | ||
|
|
d8afca956d | ||
|
|
7e59c7e818 | ||
|
|
dcf17cdade | ||
|
|
882049b3f2 | ||
|
|
c5f427f1bc | ||
|
|
242caf0d54 | ||
|
|
06d7766c0e | ||
|
|
d1a3d73c5f | ||
|
|
5c822fb4d0 | ||
|
|
6ac7b020fc | ||
|
|
bf0b0403bc | ||
|
|
af0c4d1753 | ||
|
|
564fa5f1f7 |
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -247,7 +273,6 @@ fileprivate extension SMKMessageType {
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user