Merge branch 'main' into main

This commit is contained in:
jack
2025-11-24 09:37:01 -10:00
committed by GitHub
6 changed files with 430 additions and 51 deletions
+2 -1
View File
@@ -49,7 +49,8 @@ let package = Package(
"README.md"
],
resources: [
.process("Localization")
.process("Localization"),
.process("Noise")
]
)
]
+34
View File
@@ -95,6 +95,24 @@
);
target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */;
};
C5E027A52ECCDFD700BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_macOS" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
Localization/PrimaryLocalizationKeys.json,
README.md,
);
target = 47FF23248747DD7CB666CB91 /* bitchatTests_macOS */;
};
C5E027A82ECCDFE200BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_iOS" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
Localization/PrimaryLocalizationKeys.json,
README.md,
);
target = 6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -118,6 +136,10 @@
};
A6E32D412E762EAE0032EA8A /* bitchatTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
C5E027A82ECCDFE200BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_iOS" target */,
C5E027A52ECCDFD700BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_macOS" target */,
);
path = bitchatTests;
sourceTree = "<group>";
};
@@ -213,6 +235,7 @@
buildConfigurationList = 1C27B5BA3DB46DDF0DBFEF62 /* Build configuration list for PBXNativeTarget "bitchatTests_macOS" */;
buildPhases = (
5C22AA7B9ACC5A861445C769 /* Sources */,
C5E027A42ECCDFD700BD6012 /* Resources */,
);
buildRules = (
);
@@ -245,6 +268,7 @@
buildConfigurationList = 38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */;
buildPhases = (
865C8403EF02C089369A9FCB /* Sources */,
C5E027A72ECCDFE200BD6012 /* Resources */,
);
buildRules = (
);
@@ -343,6 +367,16 @@
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */,
);
};
C5E027A42ECCDFD700BD6012 /* Resources */ = {
isa = PBXResourcesBuildPhase;
files = (
);
};
C5E027A72ECCDFE200BD6012 /* Resources */ = {
isa = PBXResourcesBuildPhase;
files = (
);
};
CD6E8F32BC38357473954F97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
files = (
+33 -20
View File
@@ -222,7 +222,7 @@ final class NoiseCipherState {
guard combinedPayload.count >= Self.NONCE_SIZE_BYTES else {
return nil
}
// Extract 4-byte nonce (big-endian)
let nonceData = combinedPayload.prefix(Self.NONCE_SIZE_BYTES)
let extractedNonce = nonceData.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> UInt64 in
@@ -233,18 +233,18 @@ final class NoiseCipherState {
}
return result
}
// Extract ciphertext (remaining bytes)
let ciphertext = combinedPayload.dropFirst(Self.NONCE_SIZE_BYTES)
return (nonce: extractedNonce, ciphertext: Data(ciphertext))
}
/// Convert nonce to 4-byte array (big-endian)
private func nonceToBytes(_ nonce: UInt64) -> Data {
var bytes = Data(count: Self.NONCE_SIZE_BYTES)
withUnsafeBytes(of: nonce.bigEndian) { ptr in
// Copy only the last 4 bytes from the 8-byte UInt64
// Copy only the last 4 bytes from the 8-byte UInt64
let sourceBytes = ptr.bindMemory(to: UInt8.self)
bytes.replaceSubrange(0..<Self.NONCE_SIZE_BYTES, with: sourceBytes.suffix(Self.NONCE_SIZE_BYTES))
}
@@ -273,7 +273,7 @@ final class NoiseCipherState {
let sealedBox = try ChaChaPoly.seal(plaintext, using: key, nonce: ChaChaPoly.Nonce(data: nonceData), authenticating: associatedData)
// increment local nonce
nonce += 1
// Create combined payload: <nonce><ciphertext>
let combinedPayload: Data
if (useExtractedNonce) {
@@ -287,7 +287,7 @@ final class NoiseCipherState {
if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.warning("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption)
}
return combinedPayload
}
@@ -316,7 +316,7 @@ final class NoiseCipherState {
SecureLogger.debug("Replay attack detected: nonce \(extractedNonce) rejected")
throw NoiseError.replayDetected
}
// Split ciphertext and tag
encryptedData = actualCiphertext.prefix(actualCiphertext.count - 16)
tag = actualCiphertext.suffix(16)
@@ -451,13 +451,13 @@ final class NoiseSymmetricState {
}
}
func split() -> (NoiseCipherState, NoiseCipherState) {
func split(useExtractedNonce: Bool) -> (NoiseCipherState, NoiseCipherState) {
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: Data(), numOutputs: 2)
let tempKey1 = SymmetricKey(data: output[0])
let tempKey2 = SymmetricKey(data: output[1])
let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: true)
let c2 = NoiseCipherState(key: tempKey2, useExtractedNonce: true)
let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: useExtractedNonce)
let c2 = NoiseCipherState(key: tempKey2, useExtractedNonce: useExtractedNonce)
return (c1, c2)
}
@@ -507,16 +507,24 @@ final class NoiseHandshakeState {
private var messagePatterns: [[NoiseMessagePattern]] = []
private var currentPattern = 0
// Test support: predetermined ephemeral keys for test vectors
private var predeterminedEphemeralKey: Curve25519.KeyAgreement.PrivateKey?
private var prologueData: Data
init(
role: NoiseRole,
pattern: NoisePattern,
keychain: KeychainManagerProtocol,
localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil,
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil
remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil,
prologue: Data = Data(),
predeterminedEphemeralKey: Curve25519.KeyAgreement.PrivateKey? = nil
) {
self.role = role
self.pattern = pattern
self.keychain = keychain
self.prologueData = prologue
self.predeterminedEphemeralKey = predeterminedEphemeralKey
// Initialize static keys
if let localKey = localStaticKey {
@@ -537,8 +545,8 @@ final class NoiseHandshakeState {
}
private func mixPreMessageKeys() {
// Mix prologue (empty for XX pattern normally)
symmetricState.mixHash(Data()) // Empty prologue for XX pattern
// Mix prologue
symmetricState.mixHash(self.prologueData)
// For XX pattern, no pre-message keys
// For IK/NK patterns, we'd mix the responder's static key here
switch pattern {
@@ -556,15 +564,20 @@ final class NoiseHandshakeState {
guard currentPattern < messagePatterns.count else {
throw NoiseError.handshakeComplete
}
var messageBuffer = Data()
let patterns = messagePatterns[currentPattern]
for pattern in patterns {
switch pattern {
case .e:
// Generate ephemeral key
localEphemeralPrivate = Curve25519.KeyAgreement.PrivateKey()
// Generate ephemeral key (or use predetermined key for tests)
if let predetermined = predeterminedEphemeralKey {
localEphemeralPrivate = predetermined
predeterminedEphemeralKey = nil
} else {
localEphemeralPrivate = Curve25519.KeyAgreement.PrivateKey()
}
localEphemeralPublic = localEphemeralPrivate!.publicKey
messageBuffer.append(localEphemeralPublic!.rawRepresentation)
symmetricState.mixHash(localEphemeralPublic!.rawRepresentation)
@@ -652,7 +665,7 @@ final class NoiseHandshakeState {
guard currentPattern < messagePatterns.count else {
throw NoiseError.handshakeComplete
}
var buffer = message
let patterns = messagePatterns[currentPattern]
@@ -776,12 +789,12 @@ final class NoiseHandshakeState {
return currentPattern >= messagePatterns.count
}
func getTransportCiphers() throws -> (send: NoiseCipherState, receive: NoiseCipherState) {
func getTransportCiphers(useExtractedNonce: Bool) throws -> (send: NoiseCipherState, receive: NoiseCipherState) {
guard isHandshakeComplete() else {
throw NoiseError.handshakeNotComplete
}
let (c1, c2) = symmetricState.split()
let (c1, c2) = symmetricState.split(useExtractedNonce: useExtractedNonce)
// Initiator uses c1 for sending, c2 for receiving
// Responder uses c2 for sending, c1 for receiving
+2 -2
View File
@@ -103,7 +103,7 @@ class NoiseSession {
// Check if handshake is complete
if handshake.isHandshakeComplete() {
// Get transport ciphers
let (send, receive) = try handshake.getTransportCiphers()
let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true)
sendCipher = send
receiveCipher = receive
@@ -129,7 +129,7 @@ class NoiseSession {
// Check if handshake is complete after writing
if handshake.isHandshakeComplete() {
// Get transport ciphers
let (send, receive) = try handshake.getTransportCiphers()
let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true)
sendCipher = send
receiveCipher = receive
+288 -28
View File
@@ -6,11 +6,53 @@
// For more information, see <https://unlicense.org>
//
import Testing
import CryptoKit
import Foundation
import Testing
@testable import bitchat
// MARK: - Test Vector Support
struct NoiseTestVector: Codable {
let protocol_name: String
let init_prologue: String
let init_static: String
let init_ephemeral: String
let init_psks: [String]?
let resp_prologue: String
let resp_static: String
let resp_ephemeral: String
let resp_psks: [String]?
let handshake_hash: String?
let messages: [TestMessage]
struct TestMessage: Codable {
let payload: String
let ciphertext: String
}
}
extension Data {
init?(hex: String) {
let cleaned = hex.replacingOccurrences(of: " ", with: "")
guard cleaned.count % 2 == 0 else { return nil }
var data = Data(capacity: cleaned.count / 2)
var index = cleaned.startIndex
while index < cleaned.endIndex {
let nextIndex = cleaned.index(index, offsetBy: 2)
guard let byte = UInt8(cleaned[index..<nextIndex], radix: 16) else { return nil }
data.append(byte)
index = nextIndex
}
self = data
}
func hexString() -> String {
map { String(format: "%02x", $0) }.joined()
}
}
struct NoiseProtocolTests {
private let aliceKey = Curve25519.KeyAgreement.PrivateKey()
@@ -61,7 +103,7 @@ struct NoiseProtocolTests {
// Bob processes message 3 and completes handshake
let finalMessage = try bobSession.processHandshakeMessage(message3!)
#expect(finalMessage == nil) // No more messages needed
#expect(finalMessage == nil) // No more messages needed
#expect(bobSession.getState() == .established)
// Verify both sessions are established
@@ -69,8 +111,12 @@ struct NoiseProtocolTests {
#expect(bobSession.isEstablished())
// Verify they have each other's static keys
#expect(aliceSession.getRemoteStaticPublicKey()?.rawRepresentation == bobKey.publicKey.rawRepresentation)
#expect(bobSession.getRemoteStaticPublicKey()?.rawRepresentation == aliceKey.publicKey.rawRepresentation)
#expect(
aliceSession.getRemoteStaticPublicKey()?.rawRepresentation
== bobKey.publicKey.rawRepresentation)
#expect(
bobSession.getRemoteStaticPublicKey()?.rawRepresentation
== aliceKey.publicKey.rawRepresentation)
}
@Test func handshakeStateValidation() throws {
@@ -98,7 +144,7 @@ struct NoiseProtocolTests {
// Alice encrypts
let ciphertext = try aliceSession.encrypt(plaintext)
#expect(ciphertext != plaintext)
#expect(ciphertext.count > plaintext.count) // Should have overhead
#expect(ciphertext.count > plaintext.count) // Should have overhead
// Bob decrypts
let decrypted = try bobSession.decrypt(ciphertext)
@@ -150,16 +196,16 @@ struct NoiseProtocolTests {
@Test func sessionManagerBasicOperations() throws {
let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain)
#expect(manager.getSession(for: alicePeerID) == nil)
_ = try manager.initiateHandshake(with: alicePeerID)
#expect(manager.getSession(for: alicePeerID) != nil)
// Get session
let retrieved = manager.getSession(for: alicePeerID)
#expect(retrieved != nil)
// Remove session
manager.removeSession(for: alicePeerID)
#expect(manager.getSession(for: alicePeerID) == nil)
@@ -190,11 +236,13 @@ struct NoiseProtocolTests {
#expect(message2 != nil)
// Continue handshake
let message3 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: message2!)
let message3 = try aliceManager.handleIncomingHandshake(
from: alicePeerID, message: message2!)
#expect(message3 != nil)
// Complete handshake
let finalMessage = try bobManager.handleIncomingHandshake(from: bobPeerID, message: message3!)
let finalMessage = try bobManager.handleIncomingHandshake(
from: bobPeerID, message: message3!)
#expect(finalMessage == nil)
// Both should have established sessions
@@ -258,11 +306,19 @@ struct NoiseProtocolTests {
@Test func sessionIsolation() throws {
// Create two separate session pairs
let aliceSession1 = NoiseSession(peerID: PeerID(str: "peer1"), role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
let bobSession1 = NoiseSession(peerID: PeerID(str: "alice1"), role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
let aliceSession1 = NoiseSession(
peerID: PeerID(str: "peer1"), role: .initiator, keychain: mockKeychain,
localStaticKey: aliceKey)
let bobSession1 = NoiseSession(
peerID: PeerID(str: "alice1"), role: .responder, keychain: mockKeychain,
localStaticKey: bobKey)
let aliceSession2 = NoiseSession(peerID: PeerID(str: "peer2"), role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
let bobSession2 = NoiseSession(peerID: PeerID(str: "alice2"), role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
let aliceSession2 = NoiseSession(
peerID: PeerID(str: "peer2"), role: .initiator, keychain: mockKeychain,
localStaticKey: aliceKey)
let bobSession2 = NoiseSession(
peerID: PeerID(str: "alice2"), role: .responder, keychain: mockKeychain,
localStaticKey: bobKey)
// Establish both pairs
try performHandshake(initiator: aliceSession1, responder: bobSession1)
@@ -305,17 +361,20 @@ struct NoiseProtocolTests {
_ = try aliceManager.decrypt(message2, from: alicePeerID)
// Simulate Bob restart by creating new manager with same key
let bobManagerRestarted = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain)
let bobManagerRestarted = NoiseSessionManager(
localStaticKey: bobKey, keychain: mockKeychain)
// Bob initiates new handshake after restart
let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: bobPeerID)
// Alice should accept the new handshake (clearing old session)
let newHandshake2 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake1)
let newHandshake2 = try aliceManager.handleIncomingHandshake(
from: alicePeerID, message: newHandshake1)
#expect(newHandshake2 != nil)
// Complete the new handshake
let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake(from: bobPeerID, message: newHandshake2!)
let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake(
from: bobPeerID, message: newHandshake2!)
#expect(newHandshake3 != nil)
_ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake3!)
@@ -328,8 +387,10 @@ struct NoiseProtocolTests {
@Test func nonceDesynchronizationRecovery() throws {
// Create two sessions
let aliceSession = NoiseSession(peerID: alicePeerID, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
let bobSession = NoiseSession(peerID: bobPeerID, role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
let aliceSession = NoiseSession(
peerID: alicePeerID, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey)
let bobSession = NoiseSession(
peerID: bobPeerID, role: .responder, keychain: mockKeychain, localStaticKey: bobKey)
// Establish sessions
try performHandshake(initiator: aliceSession, responder: bobSession)
@@ -361,7 +422,8 @@ struct NoiseProtocolTests {
let messageCount = 100
try await confirmation("All messages encrypted and decrypted", expectedCount: messageCount) { completion in
try await confirmation("All messages encrypted and decrypted", expectedCount: messageCount)
{ completion in
var encryptedMessages: [Int: Data] = [:]
// Encrypt messages sequentially to avoid nonce races in manager
for i in 0..<messageCount {
@@ -412,7 +474,7 @@ struct NoiseProtocolTests {
// Create a corrupted message
var encrypted = try aliceManager.encrypt("Test".data(using: .utf8)!, for: alicePeerID)
encrypted[10] ^= 0xFF // Corrupt the data
encrypted[10] ^= 0xFF // Corrupt the data
// Decryption should fail
if #available(macOS 14.4, iOS 17.4, *) {
@@ -454,11 +516,13 @@ struct NoiseProtocolTests {
let newHandshake1 = try aliceManager.initiateHandshake(with: alicePeerID)
// Bob should accept the new handshake even though he has a valid session
let newHandshake2 = try bobManager.handleIncomingHandshake(from: bobPeerID, message: newHandshake1)
let newHandshake2 = try bobManager.handleIncomingHandshake(
from: bobPeerID, message: newHandshake1)
#expect(newHandshake2 != nil, "Bob should accept handshake despite having valid session")
// Complete the handshake
let newHandshake3 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake2!)
let newHandshake3 = try aliceManager.handleIncomingHandshake(
from: alicePeerID, message: newHandshake2!)
#expect(newHandshake3 != nil)
_ = try bobManager.handleIncomingHandshake(from: bobPeerID, message: newHandshake3!)
@@ -489,7 +553,8 @@ struct NoiseProtocolTests {
}
// With nonce carried in packet, decryption should not throw here
let desyncMessage = try aliceManager.encrypt("This now succeeds".data(using: .utf8)!, for: alicePeerID)
let desyncMessage = try aliceManager.encrypt(
"This now succeeds".data(using: .utf8)!, for: alicePeerID)
#expect(throws: Never.self) {
try bobManager.decrypt(desyncMessage, from: bobPeerID)
}
@@ -499,11 +564,13 @@ struct NoiseProtocolTests {
let rehandshake1 = try bobManager.initiateHandshake(with: bobPeerID)
// Alice should accept despite having a "valid" (but desynced) session
let rehandshake2 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: rehandshake1)
let rehandshake2 = try aliceManager.handleIncomingHandshake(
from: alicePeerID, message: rehandshake1)
#expect(rehandshake2 != nil, "Alice should accept handshake to fix desync")
// Complete handshake
let rehandshake3 = try bobManager.handleIncomingHandshake(from: bobPeerID, message: rehandshake2!)
let rehandshake3 = try bobManager.handleIncomingHandshake(
from: bobPeerID, message: rehandshake2!)
#expect(rehandshake3 != nil)
_ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: rehandshake3!)
@@ -514,6 +581,18 @@ struct NoiseProtocolTests {
#expect(decryptedResync == testResynced)
}
// MARK: - Test Vector Tests
@Test func noiseTestVectors() throws {
// Load test vectors from bundle
let testVectors = try loadTestVectors()
for (index, testVector) in testVectors.enumerated() {
print("Running test vector \(index + 1): \(testVector.protocol_name)")
try runTestVector(testVector)
}
}
// MARK: - Helper Methods
private func performHandshake(initiator: NoiseSession, responder: NoiseSession) throws {
@@ -523,10 +602,191 @@ struct NoiseProtocolTests {
_ = try responder.processHandshakeMessage(msg3)
}
private func establishManagerSessions(aliceManager: NoiseSessionManager, bobManager: NoiseSessionManager) throws {
private func establishManagerSessions(
aliceManager: NoiseSessionManager, bobManager: NoiseSessionManager
) throws {
let msg1 = try aliceManager.initiateHandshake(with: alicePeerID)
let msg2 = try bobManager.handleIncomingHandshake(from: bobPeerID, message: msg1)!
let msg3 = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: msg2)!
_ = try bobManager.handleIncomingHandshake(from: bobPeerID, message: msg3)
}
private func loadTestVectors() throws -> [NoiseTestVector] {
// Try to load from test bundle
let testBundle = Bundle(for: MockKeychain.self)
guard let url = testBundle.url(forResource: "NoiseTestVectors", withExtension: "json")
else {
throw NSError(
domain: "NoiseTests", code: 1,
userInfo: [
NSLocalizedDescriptionKey: "Could not find NoiseTestVectors.json in test bundle"
])
}
let data = try Data(contentsOf: url)
return try JSONDecoder().decode([NoiseTestVector].self, from: data)
}
private func runTestVector(_ testVector: NoiseTestVector) throws {
// Parse test inputs
guard let initStatic = Data(hex: testVector.init_static),
let initEphemeral = Data(hex: testVector.init_ephemeral),
let respStatic = Data(hex: testVector.resp_static),
let respEphemeral = Data(hex: testVector.resp_ephemeral),
let prologue = Data(hex: testVector.init_prologue)
else {
throw NSError(
domain: "NoiseTests", code: 2,
userInfo: [NSLocalizedDescriptionKey: "Failed to parse test vector hex strings"])
}
let expectedHash = testVector.handshake_hash.flatMap { Data(hex: $0) }
// Create keys
guard
let initStaticKey = try? Curve25519.KeyAgreement.PrivateKey(
rawRepresentation: initStatic),
let initEphemeralKey = try? Curve25519.KeyAgreement.PrivateKey(
rawRepresentation: initEphemeral),
let respStaticKey = try? Curve25519.KeyAgreement.PrivateKey(
rawRepresentation: respStatic),
let respEphemeralKey = try? Curve25519.KeyAgreement.PrivateKey(
rawRepresentation: respEphemeral)
else {
throw NSError(
domain: "NoiseTests", code: 3,
userInfo: [NSLocalizedDescriptionKey: "Failed to create keys from test vectors"])
}
let keychain = MockKeychain()
// Create handshake states
let initiatorHandshake = NoiseHandshakeState(
role: .initiator,
pattern: .XX,
keychain: keychain,
localStaticKey: initStaticKey,
prologue: prologue,
predeterminedEphemeralKey: initEphemeralKey
)
let responderHandshake = NoiseHandshakeState(
role: .responder,
pattern: .XX,
keychain: keychain,
localStaticKey: respStaticKey,
prologue: prologue,
predeterminedEphemeralKey: respEphemeralKey
)
// For XX pattern, we have 3 handshake messages, then transport messages
// The test vector messages are ordered as: [msg1, msg2, msg3, transport1, transport2, ...]
guard testVector.messages.count >= 3 else {
throw NSError(
domain: "NoiseTests", code: 5,
userInfo: [NSLocalizedDescriptionKey: "Test vector must have at least 3 messages for XX pattern"])
}
// Message 1: Initiator -> Responder (e)
guard let payload1 = Data(hex: testVector.messages[0].payload),
let expectedCiphertext1 = Data(hex: testVector.messages[0].ciphertext) else {
throw NSError(
domain: "NoiseTests", code: 4,
userInfo: [NSLocalizedDescriptionKey: "Message 1: Failed to parse hex"])
}
let msg1 = try initiatorHandshake.writeMessage(payload: payload1)
#expect(!msg1.isEmpty, "Message 1 should not be empty")
#expect(msg1 == expectedCiphertext1, "Message 1 ciphertext should match expected value. Got: \(msg1.hexString()), Expected: \(expectedCiphertext1.hexString())")
let decrypted1 = try responderHandshake.readMessage(msg1)
#expect(decrypted1 == payload1, "Message 1: Decrypted payload should match original")
// Message 2: Responder -> Initiator (e, ee, s, es)
guard let payload2 = Data(hex: testVector.messages[1].payload),
let expectedCiphertext2 = Data(hex: testVector.messages[1].ciphertext) else {
throw NSError(
domain: "NoiseTests", code: 4,
userInfo: [NSLocalizedDescriptionKey: "Message 2: Failed to parse hex"])
}
let msg2 = try responderHandshake.writeMessage(payload: payload2)
#expect(!msg2.isEmpty, "Message 2 should not be empty")
#expect(msg2 == expectedCiphertext2, "Message 2 ciphertext should match expected value. Got: \(msg2.hexString()), Expected: \(expectedCiphertext2.hexString())")
let decrypted2 = try initiatorHandshake.readMessage(msg2)
#expect(decrypted2 == payload2, "Message 2: Decrypted payload should match original")
// Message 3: Initiator -> Responder (s, se)
guard let payload3 = Data(hex: testVector.messages[2].payload),
let expectedCiphertext3 = Data(hex: testVector.messages[2].ciphertext) else {
throw NSError(
domain: "NoiseTests", code: 4,
userInfo: [NSLocalizedDescriptionKey: "Message 3: Failed to parse hex"])
}
let msg3 = try initiatorHandshake.writeMessage(payload: payload3)
#expect(!msg3.isEmpty, "Message 3 should not be empty")
#expect(msg3 == expectedCiphertext3, "Message 3 ciphertext should match expected value. Got: \(msg3.hexString()), Expected: \(expectedCiphertext3.hexString())")
let decrypted3 = try responderHandshake.readMessage(msg3)
#expect(decrypted3 == payload3, "Message 3: Decrypted payload should match original")
// Verify handshake hash
let initiatorHash = initiatorHandshake.getHandshakeHash()
let responderHash = responderHandshake.getHandshakeHash()
#expect(initiatorHash == responderHash, "Initiator and responder hashes should match")
if let expectedHash = expectedHash {
#expect(
initiatorHash == expectedHash,
"Handshake hash should match expected value from test vector. Got: \(initiatorHash.hexString()), Expected: \(expectedHash.hexString())")
}
// Get transport ciphers
let (initSend, initRecv) = try initiatorHandshake.getTransportCiphers(useExtractedNonce: false)
let (respSend, respRecv) = try responderHandshake.getTransportCiphers(useExtractedNonce: false)
// Test transport messages (messages after the 3 handshake messages)
for index in 3..<testVector.messages.count {
let testMsg = testVector.messages[index]
guard let payload = Data(hex: testMsg.payload),
let expectedCiphertext = Data(hex: testMsg.ciphertext) else {
throw NSError(
domain: "NoiseTests", code: 4,
userInfo: [
NSLocalizedDescriptionKey:
"Message \(index + 1): Failed to parse payload hex"
])
}
// Alternate between responder and initiator sending
// Responder sends first transport message (since initiator sent last handshake message)
let (sender, receiver): (NoiseCipherState, NoiseCipherState)
let transportIndex = index - 3
if transportIndex % 2 == 0 {
// Even transport messages: responder sends
sender = respSend
receiver = initRecv
} else {
// Odd transport messages: initiator sends
sender = initSend
receiver = respRecv
}
// Encrypt and validate ciphertext matches expected value
let ciphertext = try sender.encrypt(plaintext: payload)
#expect(
ciphertext == expectedCiphertext,
"Message \(index + 1) ciphertext should match expected value. Got: \(ciphertext.hexString()), Expected: \(expectedCiphertext.hexString())")
// Decrypt and validate payload
let decrypted = try receiver.decrypt(ciphertext: ciphertext)
#expect(
decrypted == payload,
"Message \(index + 1): Decrypted payload should match original")
}
}
}
+71
View File
@@ -0,0 +1,71 @@
[
{
"protocol_name": "Noise_XX_25519_ChaChaPoly_SHA256",
"init_prologue": "4a6f686e2047616c74",
"init_static": "e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1",
"init_ephemeral": "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",
"resp_prologue": "4a6f686e2047616c74",
"resp_static": "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",
"resp_ephemeral": "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",
"handshake_hash": "c8e5f64e846193be2a834104c2a009868d6c9f3bd3c186299888b488b2f1f58e",
"messages": [
{
"payload": "4c756477696720766f6e204d69736573",
"ciphertext": "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c79444c756477696720766f6e204d69736573"
},
{
"payload": "4d757272617920526f746862617264",
"ciphertext": "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f14480884381cbad1f276e038c48378ffce2b65285e08d6b68aaa3629a5a8639392490e5b9bd5269c2f1e4f488ed8831161f19b7815528f8982ffe09be9b5c412f8a0db50f8814c7194e83f23dbd8d162c9326ad"
},
{
"payload": "462e20412e20486179656b",
"ciphertext": "c7195ffacac1307ff99046f219750fc47693e23c3cb08b89c2af808b444850a80ae475b9df0f169ae80a89be0865b57f58c9fea0d4ec82a286427402f113e4b6ae769a1d95941d49b25030"
},
{
"payload": "4361726c204d656e676572",
"ciphertext": "96763ed773f8e47bb3712f0e29b3060ffc956ffc146cee53d5e1df"
},
{
"payload": "4a65616e2d426170746973746520536179",
"ciphertext": "3e40f15f6f3a46ae446b253bf8b1d9ffb6ed9b174d272328ff91a7e2e5c79c07f5"
},
{
"payload": "457567656e2042f6686d20766f6e2042617765726b",
"ciphertext": "eb3f3515110702e047a6c9da4478b6ead94873c11c0f2d710ddb3f09fce024b3a58502ae3f"
}
]
},
{
"protocol_name": "Noise_XX_25519_ChaChaPoly_SHA256",
"init_prologue": "5468657265206973206e6f20726967687420616e642077726f6e672e2054686572652773206f6e6c792066756e20616e6420626f72696e672e",
"init_psks": [],
"init_static": "7dec208517a3b81a2861d7a71266d5d6dc944c5a8816634a86fe63198a0148ee",
"init_ephemeral": "a32daf21e93c0131495ce1d903181fde81cc46937daaeb990bae7c992709421e",
"resp_prologue": "5468657265206973206e6f20726967687420616e642077726f6e672e2054686572652773206f6e6c792066756e20616e6420626f72696e672e",
"resp_psks": [],
"resp_static": "4d0aed5098e3b4ef20357e9f686ce66204c792b358da2e475017d6c485304881",
"resp_ephemeral": "4eece0f195d026db035ff987597c429d3ad3bcc2944df37d649528951b2a27c5",
"messages": [
{
"payload": "d03c489139e645d0711a3c9e810d776b46a84912463fafa87b884eebf242dc34",
"ciphertext": "f9fa868ba97ab8a2686deccfaad5a484ee10a5bb85e3d1dce015a84797f92818d03c489139e645d0711a3c9e810d776b46a84912463fafa87b884eebf242dc34"
},
{
"payload": "d8190a92f7dc0c93dbea9118ba8055751fb7c6590c416ffbd419964132b99a85",
"ciphertext": "8c4e6fdb7d09d501a86f7eca5c234522751706ed409182c05cdf5f827d4dae47b81c6c5f43b025692c24391eefee725c17d8cb0fbe3e4abb8aedf42c4fd2592d4ea48ac08989d6ae8b4adae08b2c34087c808c7aa55a63c02b0fab9e930612336bd43eaea04d3c670a0a146691aa9cc9d357872320dc735dbc48580cffb553db"
},
{
"payload": "77891b19dcb92ef7c055b672c4a5aa7fdf1c84146b8b303459022729473ce254",
"ciphertext": "933ca6b5ed60df3df66121f0ab49a09e49efa45c613a86a3cecbf4c535cef2f83f72b42837b18e3572f2fdc2b74c331e2368a545cef54bdca081678ab0e9dd5348122459e0c034c851984d88ce610963d43cde6cfe73a67fbd5a63e8bfca96d0"
},
{
"payload": "d7efdf988072881941db045a42882433817555128fbf5663e56081712ec7d212",
"ciphertext": "54ef0ff0629e1aaa7685a2806ab111cba76b52331f2642276736f415868eacb69ab2577f3bda0cbf72f879685f6ed25f"
},
{
"payload": "dd7bf01a588bafb52c6cfba952e5d8fe35cc2b3f92b4730ae2474615157345ce",
"ciphertext": "356be70f110306d5c699bb834bb9d58d909e325924dfbec972e406e6f294dc63e1daebefe8a62a334facc8048ab4ad66"
}
]
}
]