Move new Noise test vectors into XCTests (WIP)

This commit is contained in:
Nadim Kobeissi
2025-11-18 19:20:16 +02:00
parent 109b7d0e03
commit 1876e85f8b
+106 -107
View File
@@ -115,7 +115,7 @@ struct NoiseProtocolName {
let dh: String = "25519" // Curve25519 let dh: String = "25519" // Curve25519
let cipher: String = "ChaChaPoly" // ChaCha20-Poly1305 let cipher: String = "ChaChaPoly" // ChaCha20-Poly1305
let hash: String = "SHA256" // SHA-256 let hash: String = "SHA256" // SHA-256
var fullName: String { var fullName: String {
"Noise_\(pattern)_\(dh)_\(cipher)_\(hash)" "Noise_\(pattern)_\(dh)_\(cipher)_\(hash)"
} }
@@ -133,59 +133,59 @@ final class NoiseCipherState {
private static let REPLAY_WINDOW_SIZE = 1024 private static let REPLAY_WINDOW_SIZE = 1024
private static let REPLAY_WINDOW_BYTES = REPLAY_WINDOW_SIZE / 8 // 128 bytes private static let REPLAY_WINDOW_BYTES = REPLAY_WINDOW_SIZE / 8 // 128 bytes
private static let HIGH_NONCE_WARNING_THRESHOLD: UInt64 = 1_000_000_000 private static let HIGH_NONCE_WARNING_THRESHOLD: UInt64 = 1_000_000_000
private var key: SymmetricKey? private var key: SymmetricKey?
private var nonce: UInt64 = 0 private var nonce: UInt64 = 0
private var useExtractedNonce: Bool = false private var useExtractedNonce: Bool = false
// Sliding window replay protection (only used when useExtractedNonce = true) // Sliding window replay protection (only used when useExtractedNonce = true)
private var highestReceivedNonce: UInt64 = 0 private var highestReceivedNonce: UInt64 = 0
private var replayWindow: [UInt8] = Array(repeating: 0, count: REPLAY_WINDOW_BYTES) private var replayWindow: [UInt8] = Array(repeating: 0, count: REPLAY_WINDOW_BYTES)
init() {} init() {}
init(key: SymmetricKey, useExtractedNonce: Bool = false) { init(key: SymmetricKey, useExtractedNonce: Bool = false) {
self.key = key self.key = key
self.useExtractedNonce = useExtractedNonce self.useExtractedNonce = useExtractedNonce
} }
deinit { deinit {
clearSensitiveData() clearSensitiveData()
} }
func initializeKey(_ key: SymmetricKey) { func initializeKey(_ key: SymmetricKey) {
self.key = key self.key = key
self.nonce = 0 self.nonce = 0
} }
func hasKey() -> Bool { func hasKey() -> Bool {
return key != nil return key != nil
} }
// MARK: - Sliding Window Replay Protection // MARK: - Sliding Window Replay Protection
/// Check if nonce is valid for replay protection /// Check if nonce is valid for replay protection
private func isValidNonce(_ receivedNonce: UInt64) -> Bool { private func isValidNonce(_ receivedNonce: UInt64) -> Bool {
if receivedNonce + UInt64(Self.REPLAY_WINDOW_SIZE) <= highestReceivedNonce { if receivedNonce + UInt64(Self.REPLAY_WINDOW_SIZE) <= highestReceivedNonce {
return false // Too old, outside window return false // Too old, outside window
} }
if receivedNonce > highestReceivedNonce { if receivedNonce > highestReceivedNonce {
return true // Always accept newer nonces return true // Always accept newer nonces
} }
let offset = Int(highestReceivedNonce - receivedNonce) let offset = Int(highestReceivedNonce - receivedNonce)
let byteIndex = offset / 8 let byteIndex = offset / 8
let bitIndex = offset % 8 let bitIndex = offset % 8
return (replayWindow[byteIndex] & (1 << bitIndex)) == 0 // Not yet seen return (replayWindow[byteIndex] & (1 << bitIndex)) == 0 // Not yet seen
} }
/// Mark nonce as seen in replay window /// Mark nonce as seen in replay window
private func markNonceAsSeen(_ receivedNonce: UInt64) { private func markNonceAsSeen(_ receivedNonce: UInt64) {
if receivedNonce > highestReceivedNonce { if receivedNonce > highestReceivedNonce {
let shift = Int(receivedNonce - highestReceivedNonce) let shift = Int(receivedNonce - highestReceivedNonce)
if shift >= Self.REPLAY_WINDOW_SIZE { if shift >= Self.REPLAY_WINDOW_SIZE {
// Clear entire window - shift is too large // Clear entire window - shift is too large
replayWindow = Array(repeating: 0, count: Self.REPLAY_WINDOW_BYTES) replayWindow = Array(repeating: 0, count: Self.REPLAY_WINDOW_BYTES)
@@ -194,18 +194,18 @@ final class NoiseCipherState {
for i in stride(from: Self.REPLAY_WINDOW_BYTES - 1, through: 0, by: -1) { for i in stride(from: Self.REPLAY_WINDOW_BYTES - 1, through: 0, by: -1) {
let sourceByteIndex = i - shift / 8 let sourceByteIndex = i - shift / 8
var newByte: UInt8 = 0 var newByte: UInt8 = 0
if sourceByteIndex >= 0 { if sourceByteIndex >= 0 {
newByte = replayWindow[sourceByteIndex] >> (shift % 8) newByte = replayWindow[sourceByteIndex] >> (shift % 8)
if sourceByteIndex > 0 && shift % 8 != 0 { if sourceByteIndex > 0 && shift % 8 != 0 {
newByte |= replayWindow[sourceByteIndex - 1] << (8 - shift % 8) newByte |= replayWindow[sourceByteIndex - 1] << (8 - shift % 8)
} }
} }
replayWindow[i] = newByte replayWindow[i] = newByte
} }
} }
highestReceivedNonce = receivedNonce highestReceivedNonce = receivedNonce
replayWindow[0] |= 1 // Mark most recent bit as seen replayWindow[0] |= 1 // Mark most recent bit as seen
} else { } else {
@@ -215,14 +215,14 @@ final class NoiseCipherState {
replayWindow[byteIndex] |= (1 << bitIndex) replayWindow[byteIndex] |= (1 << bitIndex)
} }
} }
/// Extract nonce from combined payload <nonce><ciphertext> /// Extract nonce from combined payload <nonce><ciphertext>
/// Returns tuple of (nonce, ciphertext) or nil if invalid /// Returns tuple of (nonce, ciphertext) or nil if invalid
private func extractNonceFromCiphertextPayload(_ combinedPayload: Data) throws -> (nonce: UInt64, ciphertext: Data)? { private func extractNonceFromCiphertextPayload(_ combinedPayload: Data) throws -> (nonce: UInt64, ciphertext: Data)? {
guard combinedPayload.count >= Self.NONCE_SIZE_BYTES else { guard combinedPayload.count >= Self.NONCE_SIZE_BYTES else {
return nil return nil
} }
// Extract 4-byte nonce (big-endian) // Extract 4-byte nonce (big-endian)
let nonceData = combinedPayload.prefix(Self.NONCE_SIZE_BYTES) let nonceData = combinedPayload.prefix(Self.NONCE_SIZE_BYTES)
let extractedNonce = nonceData.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> UInt64 in let extractedNonce = nonceData.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> UInt64 in
@@ -233,13 +233,13 @@ final class NoiseCipherState {
} }
return result return result
} }
// Extract ciphertext (remaining bytes) // Extract ciphertext (remaining bytes)
let ciphertext = combinedPayload.dropFirst(Self.NONCE_SIZE_BYTES) let ciphertext = combinedPayload.dropFirst(Self.NONCE_SIZE_BYTES)
return (nonce: extractedNonce, ciphertext: Data(ciphertext)) return (nonce: extractedNonce, ciphertext: Data(ciphertext))
} }
/// Convert nonce to 4-byte array (big-endian) /// Convert nonce to 4-byte array (big-endian)
private func nonceToBytes(_ nonce: UInt64) -> Data { private func nonceToBytes(_ nonce: UInt64) -> Data {
var bytes = Data(count: Self.NONCE_SIZE_BYTES) var bytes = Data(count: Self.NONCE_SIZE_BYTES)
@@ -250,30 +250,30 @@ final class NoiseCipherState {
} }
return bytes return bytes
} }
func encrypt(plaintext: Data, associatedData: Data = Data()) throws -> Data { func encrypt(plaintext: Data, associatedData: Data = Data()) throws -> Data {
guard let key = self.key else { guard let key = self.key else {
throw NoiseError.uninitializedCipher throw NoiseError.uninitializedCipher
} }
// Debug logging for nonce tracking // Debug logging for nonce tracking
let currentNonce = nonce let currentNonce = nonce
// Check if nonce exceeds 4-byte limit (UInt32 max value) // Check if nonce exceeds 4-byte limit (UInt32 max value)
guard nonce <= UInt64(UInt32.max) - 1 else { guard nonce <= UInt64(UInt32.max) - 1 else {
throw NoiseError.nonceExceeded throw NoiseError.nonceExceeded
} }
// Create nonce from counter // Create nonce from counter
var nonceData = Data(count: 12) var nonceData = Data(count: 12)
withUnsafeBytes(of: currentNonce.littleEndian) { bytes in withUnsafeBytes(of: currentNonce.littleEndian) { bytes in
nonceData.replaceSubrange(4..<12, with: bytes) nonceData.replaceSubrange(4..<12, with: bytes)
} }
let sealedBox = try ChaChaPoly.seal(plaintext, using: key, nonce: ChaChaPoly.Nonce(data: nonceData), authenticating: associatedData) let sealedBox = try ChaChaPoly.seal(plaintext, using: key, nonce: ChaChaPoly.Nonce(data: nonceData), authenticating: associatedData)
// increment local nonce // increment local nonce
nonce += 1 nonce += 1
// Create combined payload: <nonce><ciphertext> // Create combined payload: <nonce><ciphertext>
let combinedPayload: Data let combinedPayload: Data
if (useExtractedNonce) { if (useExtractedNonce) {
@@ -282,41 +282,41 @@ final class NoiseCipherState {
} else { } else {
combinedPayload = sealedBox.ciphertext + sealedBox.tag combinedPayload = sealedBox.ciphertext + sealedBox.tag
} }
// Log high nonce values that might indicate issues // Log high nonce values that might indicate issues
if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD { if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.warning("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption) SecureLogger.warning("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption)
} }
return combinedPayload return combinedPayload
} }
func decrypt(ciphertext: Data, associatedData: Data = Data()) throws -> Data { func decrypt(ciphertext: Data, associatedData: Data = Data()) throws -> Data {
guard let key = self.key else { guard let key = self.key else {
throw NoiseError.uninitializedCipher throw NoiseError.uninitializedCipher
} }
guard ciphertext.count >= 16 else { guard ciphertext.count >= 16 else {
throw NoiseError.invalidCiphertext throw NoiseError.invalidCiphertext
} }
let encryptedData: Data let encryptedData: Data
let tag: Data let tag: Data
let decryptionNonce: UInt64 let decryptionNonce: UInt64
if useExtractedNonce { if useExtractedNonce {
// Extract nonce and ciphertext from combined payload // Extract nonce and ciphertext from combined payload
guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else { guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else {
SecureLogger.debug("Decrypt failed: Could not extract nonce from payload") SecureLogger.debug("Decrypt failed: Could not extract nonce from payload")
throw NoiseError.invalidCiphertext throw NoiseError.invalidCiphertext
} }
// Validate nonce with sliding window replay protection // Validate nonce with sliding window replay protection
guard isValidNonce(extractedNonce) else { guard isValidNonce(extractedNonce) else {
SecureLogger.debug("Replay attack detected: nonce \(extractedNonce) rejected") SecureLogger.debug("Replay attack detected: nonce \(extractedNonce) rejected")
throw NoiseError.replayDetected throw NoiseError.replayDetected
} }
// Split ciphertext and tag // Split ciphertext and tag
encryptedData = actualCiphertext.prefix(actualCiphertext.count - 16) encryptedData = actualCiphertext.prefix(actualCiphertext.count - 16)
tag = actualCiphertext.suffix(16) tag = actualCiphertext.suffix(16)
@@ -327,27 +327,27 @@ final class NoiseCipherState {
tag = ciphertext.suffix(16) tag = ciphertext.suffix(16)
decryptionNonce = nonce decryptionNonce = nonce
} }
// Create nonce from counter // Create nonce from counter
var nonceData = Data(count: 12) var nonceData = Data(count: 12)
withUnsafeBytes(of: decryptionNonce.littleEndian) { bytes in withUnsafeBytes(of: decryptionNonce.littleEndian) { bytes in
nonceData.replaceSubrange(4..<12, with: bytes) nonceData.replaceSubrange(4..<12, with: bytes)
} }
let sealedBox = try ChaChaPoly.SealedBox( let sealedBox = try ChaChaPoly.SealedBox(
nonce: ChaChaPoly.Nonce(data: nonceData), nonce: ChaChaPoly.Nonce(data: nonceData),
ciphertext: encryptedData, ciphertext: encryptedData,
tag: tag tag: tag
) )
// Log high nonce values that might indicate issues // Log high nonce values that might indicate issues
if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD { if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
SecureLogger.warning("High nonce value detected: \(decryptionNonce) - consider rekeying", category: .encryption) SecureLogger.warning("High nonce value detected: \(decryptionNonce) - consider rekeying", category: .encryption)
} }
do { do {
let plaintext = try ChaChaPoly.open(sealedBox, using: key, authenticating: associatedData) let plaintext = try ChaChaPoly.open(sealedBox, using: key, authenticating: associatedData)
if useExtractedNonce { if useExtractedNonce {
// Mark nonce as seen after successful decryption // Mark nonce as seen after successful decryption
markNonceAsSeen(decryptionNonce) markNonceAsSeen(decryptionNonce)
@@ -361,16 +361,16 @@ final class NoiseCipherState {
throw error throw error
} }
} }
/// Securely clear sensitive cryptographic data from memory /// Securely clear sensitive cryptographic data from memory
func clearSensitiveData() { func clearSensitiveData() {
// Clear the symmetric key // Clear the symmetric key
key = nil key = nil
// Reset nonce // Reset nonce
nonce = 0 nonce = 0
highestReceivedNonce = 0 highestReceivedNonce = 0
// Clear replay window // Clear replay window
for i in 0..<replayWindow.count { for i in 0..<replayWindow.count {
replayWindow[i] = 0 replayWindow[i] = 0
@@ -388,10 +388,10 @@ final class NoiseSymmetricState {
private var cipherState: NoiseCipherState private var cipherState: NoiseCipherState
private var chainingKey: Data private var chainingKey: Data
private var hash: Data private var hash: Data
init(protocolName: String) { init(protocolName: String) {
self.cipherState = NoiseCipherState() self.cipherState = NoiseCipherState()
// Initialize with protocol name // Initialize with protocol name
let nameData = protocolName.data(using: .utf8)! let nameData = protocolName.data(using: .utf8)!
if nameData.count <= 32 { if nameData.count <= 32 {
@@ -401,18 +401,18 @@ final class NoiseSymmetricState {
} }
self.chainingKey = self.hash self.chainingKey = self.hash
} }
func mixKey(_ inputKeyMaterial: Data) { func mixKey(_ inputKeyMaterial: Data) {
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: inputKeyMaterial, numOutputs: 2) let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: inputKeyMaterial, numOutputs: 2)
chainingKey = output[0] chainingKey = output[0]
let tempKey = SymmetricKey(data: output[1]) let tempKey = SymmetricKey(data: output[1])
cipherState.initializeKey(tempKey) cipherState.initializeKey(tempKey)
} }
func mixHash(_ data: Data) { func mixHash(_ data: Data) {
hash = (hash + data).sha256Hash() hash = (hash + data).sha256Hash()
} }
func mixKeyAndHash(_ inputKeyMaterial: Data) { func mixKeyAndHash(_ inputKeyMaterial: Data) {
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: inputKeyMaterial, numOutputs: 3) let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: inputKeyMaterial, numOutputs: 3)
chainingKey = output[0] chainingKey = output[0]
@@ -420,15 +420,15 @@ final class NoiseSymmetricState {
let tempKey = SymmetricKey(data: output[2]) let tempKey = SymmetricKey(data: output[2])
cipherState.initializeKey(tempKey) cipherState.initializeKey(tempKey)
} }
func getHandshakeHash() -> Data { func getHandshakeHash() -> Data {
return hash return hash
} }
func hasCipherKey() -> Bool { func hasCipherKey() -> Bool {
return cipherState.hasKey() return cipherState.hasKey()
} }
func encryptAndHash(_ plaintext: Data) throws -> Data { func encryptAndHash(_ plaintext: Data) throws -> Data {
if cipherState.hasKey() { if cipherState.hasKey() {
let ciphertext = try cipherState.encrypt(plaintext: plaintext, associatedData: hash) let ciphertext = try cipherState.encrypt(plaintext: plaintext, associatedData: hash)
@@ -439,7 +439,7 @@ final class NoiseSymmetricState {
return plaintext return plaintext
} }
} }
func decryptAndHash(_ ciphertext: Data) throws -> Data { func decryptAndHash(_ ciphertext: Data) throws -> Data {
if cipherState.hasKey() { if cipherState.hasKey() {
let plaintext = try cipherState.decrypt(ciphertext: ciphertext, associatedData: hash) let plaintext = try cipherState.decrypt(ciphertext: ciphertext, associatedData: hash)
@@ -450,26 +450,26 @@ final class NoiseSymmetricState {
return ciphertext return ciphertext
} }
} }
func split() -> (NoiseCipherState, NoiseCipherState) { func split() -> (NoiseCipherState, NoiseCipherState) {
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: Data(), numOutputs: 2) let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: Data(), numOutputs: 2)
let tempKey1 = SymmetricKey(data: output[0]) let tempKey1 = SymmetricKey(data: output[0])
let tempKey2 = SymmetricKey(data: output[1]) let tempKey2 = SymmetricKey(data: output[1])
let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: true) let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: true)
let c2 = NoiseCipherState(key: tempKey2, useExtractedNonce: true) let c2 = NoiseCipherState(key: tempKey2, useExtractedNonce: true)
return (c1, c2) return (c1, c2)
} }
// HKDF implementation // HKDF implementation
private func hkdf(chainingKey: Data, inputKeyMaterial: Data, numOutputs: Int) -> [Data] { private func hkdf(chainingKey: Data, inputKeyMaterial: Data, numOutputs: Int) -> [Data] {
let tempKey = HMAC<SHA256>.authenticationCode(for: inputKeyMaterial, using: SymmetricKey(data: chainingKey)) let tempKey = HMAC<SHA256>.authenticationCode(for: inputKeyMaterial, using: SymmetricKey(data: chainingKey))
let tempKeyData = Data(tempKey) let tempKeyData = Data(tempKey)
var outputs: [Data] = [] var outputs: [Data] = []
var currentOutput = Data() var currentOutput = Data()
for i in 1...numOutputs { for i in 1...numOutputs {
currentOutput = Data(HMAC<SHA256>.authenticationCode( currentOutput = Data(HMAC<SHA256>.authenticationCode(
for: currentOutput + Data([UInt8(i)]), for: currentOutput + Data([UInt8(i)]),
@@ -477,7 +477,7 @@ final class NoiseSymmetricState {
)) ))
outputs.append(currentOutput) outputs.append(currentOutput)
} }
return outputs return outputs
} }
} }
@@ -493,24 +493,24 @@ final class NoiseHandshakeState {
private let pattern: NoisePattern private let pattern: NoisePattern
private let keychain: KeychainManagerProtocol private let keychain: KeychainManagerProtocol
private var symmetricState: NoiseSymmetricState private var symmetricState: NoiseSymmetricState
// Keys // Keys
private var localStaticPrivate: Curve25519.KeyAgreement.PrivateKey? private var localStaticPrivate: Curve25519.KeyAgreement.PrivateKey?
private var localStaticPublic: Curve25519.KeyAgreement.PublicKey? private var localStaticPublic: Curve25519.KeyAgreement.PublicKey?
private var localEphemeralPrivate: Curve25519.KeyAgreement.PrivateKey? private var localEphemeralPrivate: Curve25519.KeyAgreement.PrivateKey?
private var localEphemeralPublic: Curve25519.KeyAgreement.PublicKey? private var localEphemeralPublic: Curve25519.KeyAgreement.PublicKey?
private var remoteStaticPublic: Curve25519.KeyAgreement.PublicKey? private var remoteStaticPublic: Curve25519.KeyAgreement.PublicKey?
private var remoteEphemeralPublic: Curve25519.KeyAgreement.PublicKey? private var remoteEphemeralPublic: Curve25519.KeyAgreement.PublicKey?
// Message patterns // Message patterns
private var messagePatterns: [[NoiseMessagePattern]] = [] private var messagePatterns: [[NoiseMessagePattern]] = []
private var currentPattern = 0 private var currentPattern = 0
// Test support: predetermined ephemeral keys for test vectors // Test support: predetermined ephemeral keys for test vectors
private var predeterminedEphemeralKey: Curve25519.KeyAgreement.PrivateKey? private var predeterminedEphemeralKey: Curve25519.KeyAgreement.PrivateKey?
private var prologueData: Data private var prologueData: Data
init( init(
role: NoiseRole, role: NoiseRole,
pattern: NoisePattern, pattern: NoisePattern,
@@ -523,28 +523,27 @@ final class NoiseHandshakeState {
self.role = role self.role = role
self.pattern = pattern self.pattern = pattern
self.keychain = keychain self.keychain = keychain
self.prologueData = prologue self.prologueData = prologue
self.predeterminedEphemeralKey = predeterminedEphemeralKey self.predeterminedEphemeralKey = predeterminedEphemeralKey
// Initialize static keys // Initialize static keys
if let localKey = localStaticKey { if let localKey = localStaticKey {
self.localStaticPrivate = localKey self.localStaticPrivate = localKey
self.localStaticPublic = localKey.publicKey self.localStaticPublic = localKey.publicKey
} }
self.remoteStaticPublic = remoteStaticKey self.remoteStaticPublic = remoteStaticKey
// Initialize protocol name // Initialize protocol name
let protocolName = NoiseProtocolName(pattern: pattern.patternName) let protocolName = NoiseProtocolName(pattern: pattern.patternName)
self.symmetricState = NoiseSymmetricState(protocolName: protocolName.fullName) self.symmetricState = NoiseSymmetricState(protocolName: protocolName.fullName)
// Initialize message patterns // Initialize message patterns
self.messagePatterns = pattern.messagePatterns self.messagePatterns = pattern.messagePatterns
// Mix pre-message keys according to pattern // Mix pre-message keys according to pattern
mixPreMessageKeys() mixPreMessageKeys()
} }
private func mixPreMessageKeys() { private func mixPreMessageKeys() {
// Mix prologue // Mix prologue
symmetricState.mixHash(self.prologueData) symmetricState.mixHash(self.prologueData)
@@ -560,15 +559,15 @@ final class NoiseHandshakeState {
} }
} }
} }
func writeMessage(payload: Data = Data()) throws -> Data { func writeMessage(payload: Data = Data()) throws -> Data {
guard currentPattern < messagePatterns.count else { guard currentPattern < messagePatterns.count else {
throw NoiseError.handshakeComplete throw NoiseError.handshakeComplete
} }
var messageBuffer = Data() var messageBuffer = Data()
let patterns = messagePatterns[currentPattern] let patterns = messagePatterns[currentPattern]
for pattern in patterns { for pattern in patterns {
switch pattern { switch pattern {
case .e: case .e:
@@ -582,7 +581,7 @@ final class NoiseHandshakeState {
localEphemeralPublic = localEphemeralPrivate!.publicKey localEphemeralPublic = localEphemeralPrivate!.publicKey
messageBuffer.append(localEphemeralPublic!.rawRepresentation) messageBuffer.append(localEphemeralPublic!.rawRepresentation)
symmetricState.mixHash(localEphemeralPublic!.rawRepresentation) symmetricState.mixHash(localEphemeralPublic!.rawRepresentation)
case .s: case .s:
// Send static key (encrypted if cipher is initialized) // Send static key (encrypted if cipher is initialized)
guard let staticPublic = localStaticPublic else { guard let staticPublic = localStaticPublic else {
@@ -590,7 +589,7 @@ final class NoiseHandshakeState {
} }
let encrypted = try symmetricState.encryptAndHash(staticPublic.rawRepresentation) let encrypted = try symmetricState.encryptAndHash(staticPublic.rawRepresentation)
messageBuffer.append(encrypted) messageBuffer.append(encrypted)
case .ee: case .ee:
// DH(local ephemeral, remote ephemeral) // DH(local ephemeral, remote ephemeral)
guard let localEphemeral = localEphemeralPrivate, guard let localEphemeral = localEphemeralPrivate,
@@ -602,7 +601,7 @@ final class NoiseHandshakeState {
symmetricState.mixKey(sharedData) symmetricState.mixKey(sharedData)
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) keychain.secureClear(&sharedData)
case .es: case .es:
// DH(ephemeral, static) - direction depends on role // DH(ephemeral, static) - direction depends on role
if role == .initiator { if role == .initiator {
@@ -620,7 +619,7 @@ final class NoiseHandshakeState {
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral) let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral)
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) }) symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
} }
case .se: case .se:
// DH(static, ephemeral) - direction depends on role // DH(static, ephemeral) - direction depends on role
if role == .initiator { if role == .initiator {
@@ -638,7 +637,7 @@ final class NoiseHandshakeState {
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic) let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic)
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) }) symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
} }
case .ss: case .ss:
// DH(static, static) // DH(static, static)
guard let localStatic = localStaticPrivate, guard let localStatic = localStaticPrivate,
@@ -652,24 +651,24 @@ final class NoiseHandshakeState {
keychain.secureClear(&sharedData) keychain.secureClear(&sharedData)
} }
} }
// Encrypt payload // Encrypt payload
let encryptedPayload = try symmetricState.encryptAndHash(payload) let encryptedPayload = try symmetricState.encryptAndHash(payload)
messageBuffer.append(encryptedPayload) messageBuffer.append(encryptedPayload)
currentPattern += 1 currentPattern += 1
return messageBuffer return messageBuffer
} }
func readMessage(_ message: Data, expectedPayloadLength: Int = 0) throws -> Data { func readMessage(_ message: Data, expectedPayloadLength: Int = 0) throws -> Data {
guard currentPattern < messagePatterns.count else { guard currentPattern < messagePatterns.count else {
throw NoiseError.handshakeComplete throw NoiseError.handshakeComplete
} }
var buffer = message var buffer = message
let patterns = messagePatterns[currentPattern] let patterns = messagePatterns[currentPattern]
for pattern in patterns { for pattern in patterns {
switch pattern { switch pattern {
case .e: case .e:
@@ -679,7 +678,7 @@ final class NoiseHandshakeState {
} }
let ephemeralData = buffer.prefix(32) let ephemeralData = buffer.prefix(32)
buffer = buffer.dropFirst(32) buffer = buffer.dropFirst(32)
do { do {
remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData) remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData)
} catch { } catch {
@@ -687,7 +686,7 @@ final class NoiseHandshakeState {
throw NoiseError.invalidMessage throw NoiseError.invalidMessage
} }
symmetricState.mixHash(ephemeralData) symmetricState.mixHash(ephemeralData)
case .s: case .s:
// Read static key (may be encrypted) // Read static key (may be encrypted)
let keyLength = symmetricState.hasCipherKey() ? 48 : 32 // 32 + 16 byte tag if encrypted let keyLength = symmetricState.hasCipherKey() ? 48 : 32 // 32 + 16 byte tag if encrypted
@@ -703,20 +702,20 @@ final class NoiseHandshakeState {
SecureLogger.error(.authenticationFailed(peerID: "Unknown - handshake")) SecureLogger.error(.authenticationFailed(peerID: "Unknown - handshake"))
throw NoiseError.authenticationFailure throw NoiseError.authenticationFailure
} }
case .ee, .es, .se, .ss: case .ee, .es, .se, .ss:
// Same DH operations as in writeMessage // Same DH operations as in writeMessage
try performDHOperation(pattern) try performDHOperation(pattern)
} }
} }
// Decrypt payload // Decrypt payload
let payload = try symmetricState.decryptAndHash(buffer) let payload = try symmetricState.decryptAndHash(buffer)
currentPattern += 1 currentPattern += 1
return payload return payload
} }
private func performDHOperation(_ pattern: NoiseMessagePattern) throws { private func performDHOperation(_ pattern: NoiseMessagePattern) throws {
switch pattern { switch pattern {
case .ee: case .ee:
@@ -726,7 +725,7 @@ final class NoiseHandshakeState {
} }
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteEphemeral) let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteEphemeral)
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) }) symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
case .es: case .es:
if role == .initiator { if role == .initiator {
guard let localEphemeral = localEphemeralPrivate, guard let localEphemeral = localEphemeralPrivate,
@@ -749,7 +748,7 @@ final class NoiseHandshakeState {
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) keychain.secureClear(&sharedData)
} }
case .se: case .se:
if role == .initiator { if role == .initiator {
guard let localStatic = localStaticPrivate, guard let localStatic = localStaticPrivate,
@@ -772,7 +771,7 @@ final class NoiseHandshakeState {
// Clear sensitive shared secret // Clear sensitive shared secret
keychain.secureClear(&sharedData) keychain.secureClear(&sharedData)
} }
case .ss: case .ss:
guard let localStatic = localStaticPrivate, guard let localStatic = localStaticPrivate,
let remoteStatic = remoteStaticPublic else { let remoteStatic = remoteStaticPublic else {
@@ -780,32 +779,32 @@ final class NoiseHandshakeState {
} }
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteStatic) let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteStatic)
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) }) symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
case .e, .s: case .e, .s:
break break
} }
} }
func isHandshakeComplete() -> Bool { func isHandshakeComplete() -> Bool {
return currentPattern >= messagePatterns.count return currentPattern >= messagePatterns.count
} }
func getTransportCiphers() throws -> (send: NoiseCipherState, receive: NoiseCipherState) { func getTransportCiphers() throws -> (send: NoiseCipherState, receive: NoiseCipherState) {
guard isHandshakeComplete() else { guard isHandshakeComplete() else {
throw NoiseError.handshakeNotComplete throw NoiseError.handshakeNotComplete
} }
let (c1, c2) = symmetricState.split() let (c1, c2) = symmetricState.split()
// Initiator uses c1 for sending, c2 for receiving // Initiator uses c1 for sending, c2 for receiving
// Responder uses c2 for sending, c1 for receiving // Responder uses c2 for sending, c1 for receiving
return role == .initiator ? (c1, c2) : (c2, c1) return role == .initiator ? (c1, c2) : (c2, c1)
} }
func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? { func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? {
return remoteStaticPublic return remoteStaticPublic
} }
func getHandshakeHash() -> Data { func getHandshakeHash() -> Data {
return symmetricState.getHandshakeHash() return symmetricState.getHandshakeHash()
} }
@@ -821,7 +820,7 @@ extension NoisePattern {
case .NK: return "NK" case .NK: return "NK"
} }
} }
var messagePatterns: [[NoiseMessagePattern]] { var messagePatterns: [[NoiseMessagePattern]] {
switch self { switch self {
case .XX: case .XX:
@@ -870,12 +869,12 @@ extension NoiseHandshakeState {
guard keyData.count == 32 else { guard keyData.count == 32 else {
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
// Check for all-zero key (point at infinity) // Check for all-zero key (point at infinity)
if keyData.allSatisfy({ $0 == 0 }) { if keyData.allSatisfy({ $0 == 0 }) {
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
// Check for low-order points that could enable small subgroup attacks // Check for low-order points that could enable small subgroup attacks
// These are the known bad points for Curve25519 // These are the known bad points for Curve25519
let lowOrderPoints: [Data] = [ let lowOrderPoints: [Data] = [
@@ -896,13 +895,13 @@ extension NoiseHandshakeState {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) // Another bad point 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) // Another bad point
] ]
// Check against known bad points // Check against known bad points
if lowOrderPoints.contains(keyData) { if lowOrderPoints.contains(keyData) {
SecureLogger.warning("Low-order point detected", category: .security) SecureLogger.warning("Low-order point detected", category: .security)
throw NoiseError.invalidPublicKey throw NoiseError.invalidPublicKey
} }
// Try to create the key - CryptoKit will validate curve points internally // Try to create the key - CryptoKit will validate curve points internally
do { do {
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData) let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData)