mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 12:45:19 +00:00
Initial work on adding Noise test vectors
I only became aware halfway through working on these that `bitchatTests/Noise` exists, so the next step is to integrate them over there.
This commit is contained in:
+122
-106
@@ -77,7 +77,9 @@
|
||||
/// - Noise Specification: http://www.noiseprotocol.org/noise.html
|
||||
///
|
||||
|
||||
#if !NOISE_TESTS
|
||||
import BitLogger
|
||||
#endif
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
@@ -115,7 +117,7 @@ struct NoiseProtocolName {
|
||||
let dh: String = "25519" // Curve25519
|
||||
let cipher: String = "ChaChaPoly" // ChaCha20-Poly1305
|
||||
let hash: String = "SHA256" // SHA-256
|
||||
|
||||
|
||||
var fullName: String {
|
||||
"Noise_\(pattern)_\(dh)_\(cipher)_\(hash)"
|
||||
}
|
||||
@@ -133,59 +135,59 @@ final class NoiseCipherState {
|
||||
private static let REPLAY_WINDOW_SIZE = 1024
|
||||
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 var key: SymmetricKey?
|
||||
private var nonce: UInt64 = 0
|
||||
private var useExtractedNonce: Bool = false
|
||||
|
||||
|
||||
// Sliding window replay protection (only used when useExtractedNonce = true)
|
||||
private var highestReceivedNonce: UInt64 = 0
|
||||
private var replayWindow: [UInt8] = Array(repeating: 0, count: REPLAY_WINDOW_BYTES)
|
||||
|
||||
|
||||
init() {}
|
||||
|
||||
|
||||
init(key: SymmetricKey, useExtractedNonce: Bool = false) {
|
||||
self.key = key
|
||||
self.useExtractedNonce = useExtractedNonce
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
clearSensitiveData()
|
||||
}
|
||||
|
||||
|
||||
func initializeKey(_ key: SymmetricKey) {
|
||||
self.key = key
|
||||
self.nonce = 0
|
||||
}
|
||||
|
||||
|
||||
func hasKey() -> Bool {
|
||||
return key != nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Sliding Window Replay Protection
|
||||
|
||||
|
||||
/// Check if nonce is valid for replay protection
|
||||
private func isValidNonce(_ receivedNonce: UInt64) -> Bool {
|
||||
if receivedNonce + UInt64(Self.REPLAY_WINDOW_SIZE) <= highestReceivedNonce {
|
||||
return false // Too old, outside window
|
||||
}
|
||||
|
||||
|
||||
if receivedNonce > highestReceivedNonce {
|
||||
return true // Always accept newer nonces
|
||||
}
|
||||
|
||||
|
||||
let offset = Int(highestReceivedNonce - receivedNonce)
|
||||
let byteIndex = offset / 8
|
||||
let bitIndex = offset % 8
|
||||
|
||||
|
||||
return (replayWindow[byteIndex] & (1 << bitIndex)) == 0 // Not yet seen
|
||||
}
|
||||
|
||||
|
||||
/// Mark nonce as seen in replay window
|
||||
private func markNonceAsSeen(_ receivedNonce: UInt64) {
|
||||
if receivedNonce > highestReceivedNonce {
|
||||
let shift = Int(receivedNonce - highestReceivedNonce)
|
||||
|
||||
|
||||
if shift >= Self.REPLAY_WINDOW_SIZE {
|
||||
// Clear entire window - shift is too large
|
||||
replayWindow = Array(repeating: 0, count: Self.REPLAY_WINDOW_BYTES)
|
||||
@@ -194,18 +196,18 @@ final class NoiseCipherState {
|
||||
for i in stride(from: Self.REPLAY_WINDOW_BYTES - 1, through: 0, by: -1) {
|
||||
let sourceByteIndex = i - shift / 8
|
||||
var newByte: UInt8 = 0
|
||||
|
||||
|
||||
if sourceByteIndex >= 0 {
|
||||
newByte = replayWindow[sourceByteIndex] >> (shift % 8)
|
||||
if sourceByteIndex > 0 && shift % 8 != 0 {
|
||||
newByte |= replayWindow[sourceByteIndex - 1] << (8 - shift % 8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
replayWindow[i] = newByte
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
highestReceivedNonce = receivedNonce
|
||||
replayWindow[0] |= 1 // Mark most recent bit as seen
|
||||
} else {
|
||||
@@ -215,7 +217,7 @@ final class NoiseCipherState {
|
||||
replayWindow[byteIndex] |= (1 << bitIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Extract nonce from combined payload <nonce><ciphertext>
|
||||
/// Returns tuple of (nonce, ciphertext) or nil if invalid
|
||||
private func extractNonceFromCiphertextPayload(_ combinedPayload: Data) throws -> (nonce: UInt64, ciphertext: Data)? {
|
||||
@@ -244,36 +246,36 @@ final class NoiseCipherState {
|
||||
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))
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
|
||||
func encrypt(plaintext: Data, associatedData: Data = Data()) throws -> Data {
|
||||
guard let key = self.key else {
|
||||
throw NoiseError.uninitializedCipher
|
||||
}
|
||||
|
||||
|
||||
// Debug logging for nonce tracking
|
||||
let currentNonce = nonce
|
||||
|
||||
|
||||
// Check if nonce exceeds 4-byte limit (UInt32 max value)
|
||||
guard nonce <= UInt64(UInt32.max) - 1 else {
|
||||
throw NoiseError.nonceExceeded
|
||||
}
|
||||
|
||||
|
||||
// Create nonce from counter
|
||||
var nonceData = Data(count: 12)
|
||||
withUnsafeBytes(of: currentNonce.littleEndian) { bytes in
|
||||
nonceData.replaceSubrange(4..<12, with: bytes)
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
@@ -282,35 +284,35 @@ final class NoiseCipherState {
|
||||
} else {
|
||||
combinedPayload = sealedBox.ciphertext + sealedBox.tag
|
||||
}
|
||||
|
||||
|
||||
// Log high nonce values that might indicate issues
|
||||
if currentNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
|
||||
SecureLogger.warning("High nonce value detected: \(currentNonce) - consider rekeying", category: .encryption)
|
||||
}
|
||||
|
||||
|
||||
return combinedPayload
|
||||
}
|
||||
|
||||
|
||||
func decrypt(ciphertext: Data, associatedData: Data = Data()) throws -> Data {
|
||||
guard let key = self.key else {
|
||||
throw NoiseError.uninitializedCipher
|
||||
}
|
||||
|
||||
|
||||
guard ciphertext.count >= 16 else {
|
||||
throw NoiseError.invalidCiphertext
|
||||
}
|
||||
|
||||
|
||||
let encryptedData: Data
|
||||
let tag: Data
|
||||
let decryptionNonce: UInt64
|
||||
|
||||
|
||||
if useExtractedNonce {
|
||||
// Extract nonce and ciphertext from combined payload
|
||||
guard let (extractedNonce, actualCiphertext) = try extractNonceFromCiphertextPayload(ciphertext) else {
|
||||
SecureLogger.debug("Decrypt failed: Could not extract nonce from payload")
|
||||
throw NoiseError.invalidCiphertext
|
||||
}
|
||||
|
||||
|
||||
// Validate nonce with sliding window replay protection
|
||||
guard isValidNonce(extractedNonce) else {
|
||||
SecureLogger.debug("Replay attack detected: nonce \(extractedNonce) rejected")
|
||||
@@ -327,27 +329,27 @@ final class NoiseCipherState {
|
||||
tag = ciphertext.suffix(16)
|
||||
decryptionNonce = nonce
|
||||
}
|
||||
|
||||
|
||||
// Create nonce from counter
|
||||
var nonceData = Data(count: 12)
|
||||
withUnsafeBytes(of: decryptionNonce.littleEndian) { bytes in
|
||||
nonceData.replaceSubrange(4..<12, with: bytes)
|
||||
}
|
||||
|
||||
|
||||
let sealedBox = try ChaChaPoly.SealedBox(
|
||||
nonce: ChaChaPoly.Nonce(data: nonceData),
|
||||
ciphertext: encryptedData,
|
||||
tag: tag
|
||||
)
|
||||
|
||||
|
||||
// Log high nonce values that might indicate issues
|
||||
if decryptionNonce > Self.HIGH_NONCE_WARNING_THRESHOLD {
|
||||
SecureLogger.warning("High nonce value detected: \(decryptionNonce) - consider rekeying", category: .encryption)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
let plaintext = try ChaChaPoly.open(sealedBox, using: key, authenticating: associatedData)
|
||||
|
||||
|
||||
if useExtractedNonce {
|
||||
// Mark nonce as seen after successful decryption
|
||||
markNonceAsSeen(decryptionNonce)
|
||||
@@ -361,16 +363,16 @@ final class NoiseCipherState {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Securely clear sensitive cryptographic data from memory
|
||||
func clearSensitiveData() {
|
||||
// Clear the symmetric key
|
||||
key = nil
|
||||
|
||||
|
||||
// Reset nonce
|
||||
nonce = 0
|
||||
highestReceivedNonce = 0
|
||||
|
||||
|
||||
// Clear replay window
|
||||
for i in 0..<replayWindow.count {
|
||||
replayWindow[i] = 0
|
||||
@@ -388,10 +390,10 @@ final class NoiseSymmetricState {
|
||||
private var cipherState: NoiseCipherState
|
||||
private var chainingKey: Data
|
||||
private var hash: Data
|
||||
|
||||
|
||||
init(protocolName: String) {
|
||||
self.cipherState = NoiseCipherState()
|
||||
|
||||
|
||||
// Initialize with protocol name
|
||||
let nameData = protocolName.data(using: .utf8)!
|
||||
if nameData.count <= 32 {
|
||||
@@ -401,18 +403,18 @@ final class NoiseSymmetricState {
|
||||
}
|
||||
self.chainingKey = self.hash
|
||||
}
|
||||
|
||||
|
||||
func mixKey(_ inputKeyMaterial: Data) {
|
||||
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: inputKeyMaterial, numOutputs: 2)
|
||||
chainingKey = output[0]
|
||||
let tempKey = SymmetricKey(data: output[1])
|
||||
cipherState.initializeKey(tempKey)
|
||||
}
|
||||
|
||||
|
||||
func mixHash(_ data: Data) {
|
||||
hash = (hash + data).sha256Hash()
|
||||
}
|
||||
|
||||
|
||||
func mixKeyAndHash(_ inputKeyMaterial: Data) {
|
||||
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: inputKeyMaterial, numOutputs: 3)
|
||||
chainingKey = output[0]
|
||||
@@ -420,15 +422,15 @@ final class NoiseSymmetricState {
|
||||
let tempKey = SymmetricKey(data: output[2])
|
||||
cipherState.initializeKey(tempKey)
|
||||
}
|
||||
|
||||
|
||||
func getHandshakeHash() -> Data {
|
||||
return hash
|
||||
}
|
||||
|
||||
|
||||
func hasCipherKey() -> Bool {
|
||||
return cipherState.hasKey()
|
||||
}
|
||||
|
||||
|
||||
func encryptAndHash(_ plaintext: Data) throws -> Data {
|
||||
if cipherState.hasKey() {
|
||||
let ciphertext = try cipherState.encrypt(plaintext: plaintext, associatedData: hash)
|
||||
@@ -439,7 +441,7 @@ final class NoiseSymmetricState {
|
||||
return plaintext
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func decryptAndHash(_ ciphertext: Data) throws -> Data {
|
||||
if cipherState.hasKey() {
|
||||
let plaintext = try cipherState.decrypt(ciphertext: ciphertext, associatedData: hash)
|
||||
@@ -450,26 +452,26 @@ final class NoiseSymmetricState {
|
||||
return ciphertext
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func split() -> (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)
|
||||
|
||||
|
||||
return (c1, c2)
|
||||
}
|
||||
|
||||
|
||||
// HKDF implementation
|
||||
private func hkdf(chainingKey: Data, inputKeyMaterial: Data, numOutputs: Int) -> [Data] {
|
||||
let tempKey = HMAC<SHA256>.authenticationCode(for: inputKeyMaterial, using: SymmetricKey(data: chainingKey))
|
||||
let tempKeyData = Data(tempKey)
|
||||
|
||||
|
||||
var outputs: [Data] = []
|
||||
var currentOutput = Data()
|
||||
|
||||
|
||||
for i in 1...numOutputs {
|
||||
currentOutput = Data(HMAC<SHA256>.authenticationCode(
|
||||
for: currentOutput + Data([UInt8(i)]),
|
||||
@@ -477,7 +479,7 @@ final class NoiseSymmetricState {
|
||||
))
|
||||
outputs.append(currentOutput)
|
||||
}
|
||||
|
||||
|
||||
return outputs
|
||||
}
|
||||
}
|
||||
@@ -493,52 +495,61 @@ final class NoiseHandshakeState {
|
||||
private let pattern: NoisePattern
|
||||
private let keychain: KeychainManagerProtocol
|
||||
private var symmetricState: NoiseSymmetricState
|
||||
|
||||
|
||||
// Keys
|
||||
private var localStaticPrivate: Curve25519.KeyAgreement.PrivateKey?
|
||||
private var localStaticPublic: Curve25519.KeyAgreement.PublicKey?
|
||||
private var localEphemeralPrivate: Curve25519.KeyAgreement.PrivateKey?
|
||||
private var localEphemeralPublic: Curve25519.KeyAgreement.PublicKey?
|
||||
|
||||
|
||||
private var remoteStaticPublic: Curve25519.KeyAgreement.PublicKey?
|
||||
private var remoteEphemeralPublic: Curve25519.KeyAgreement.PublicKey?
|
||||
|
||||
|
||||
// Message patterns
|
||||
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 {
|
||||
self.localStaticPrivate = localKey
|
||||
self.localStaticPublic = localKey.publicKey
|
||||
}
|
||||
self.remoteStaticPublic = remoteStaticKey
|
||||
|
||||
|
||||
// Initialize protocol name
|
||||
let protocolName = NoiseProtocolName(pattern: pattern.patternName)
|
||||
self.symmetricState = NoiseSymmetricState(protocolName: protocolName.fullName)
|
||||
|
||||
|
||||
// Initialize message patterns
|
||||
self.messagePatterns = pattern.messagePatterns
|
||||
|
||||
|
||||
// Mix pre-message keys according to pattern
|
||||
mixPreMessageKeys()
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
@@ -551,24 +562,29 @@ final class NoiseHandshakeState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func writeMessage(payload: Data = Data()) throws -> Data {
|
||||
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)
|
||||
|
||||
|
||||
case .s:
|
||||
// Send static key (encrypted if cipher is initialized)
|
||||
guard let staticPublic = localStaticPublic else {
|
||||
@@ -576,7 +592,7 @@ final class NoiseHandshakeState {
|
||||
}
|
||||
let encrypted = try symmetricState.encryptAndHash(staticPublic.rawRepresentation)
|
||||
messageBuffer.append(encrypted)
|
||||
|
||||
|
||||
case .ee:
|
||||
// DH(local ephemeral, remote ephemeral)
|
||||
guard let localEphemeral = localEphemeralPrivate,
|
||||
@@ -588,7 +604,7 @@ final class NoiseHandshakeState {
|
||||
symmetricState.mixKey(sharedData)
|
||||
// Clear sensitive shared secret
|
||||
keychain.secureClear(&sharedData)
|
||||
|
||||
|
||||
case .es:
|
||||
// DH(ephemeral, static) - direction depends on role
|
||||
if role == .initiator {
|
||||
@@ -606,7 +622,7 @@ final class NoiseHandshakeState {
|
||||
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
||||
}
|
||||
|
||||
|
||||
case .se:
|
||||
// DH(static, ephemeral) - direction depends on role
|
||||
if role == .initiator {
|
||||
@@ -624,7 +640,7 @@ final class NoiseHandshakeState {
|
||||
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteStatic)
|
||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
||||
}
|
||||
|
||||
|
||||
case .ss:
|
||||
// DH(static, static)
|
||||
guard let localStatic = localStaticPrivate,
|
||||
@@ -638,24 +654,24 @@ final class NoiseHandshakeState {
|
||||
keychain.secureClear(&sharedData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Encrypt payload
|
||||
let encryptedPayload = try symmetricState.encryptAndHash(payload)
|
||||
messageBuffer.append(encryptedPayload)
|
||||
|
||||
|
||||
currentPattern += 1
|
||||
return messageBuffer
|
||||
}
|
||||
|
||||
|
||||
func readMessage(_ message: Data, expectedPayloadLength: Int = 0) throws -> Data {
|
||||
|
||||
|
||||
guard currentPattern < messagePatterns.count else {
|
||||
throw NoiseError.handshakeComplete
|
||||
}
|
||||
|
||||
|
||||
var buffer = message
|
||||
let patterns = messagePatterns[currentPattern]
|
||||
|
||||
|
||||
for pattern in patterns {
|
||||
switch pattern {
|
||||
case .e:
|
||||
@@ -665,7 +681,7 @@ final class NoiseHandshakeState {
|
||||
}
|
||||
let ephemeralData = buffer.prefix(32)
|
||||
buffer = buffer.dropFirst(32)
|
||||
|
||||
|
||||
do {
|
||||
remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData)
|
||||
} catch {
|
||||
@@ -673,7 +689,7 @@ final class NoiseHandshakeState {
|
||||
throw NoiseError.invalidMessage
|
||||
}
|
||||
symmetricState.mixHash(ephemeralData)
|
||||
|
||||
|
||||
case .s:
|
||||
// Read static key (may be encrypted)
|
||||
let keyLength = symmetricState.hasCipherKey() ? 48 : 32 // 32 + 16 byte tag if encrypted
|
||||
@@ -689,20 +705,20 @@ final class NoiseHandshakeState {
|
||||
SecureLogger.error(.authenticationFailed(peerID: "Unknown - handshake"))
|
||||
throw NoiseError.authenticationFailure
|
||||
}
|
||||
|
||||
|
||||
case .ee, .es, .se, .ss:
|
||||
// Same DH operations as in writeMessage
|
||||
try performDHOperation(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Decrypt payload
|
||||
let payload = try symmetricState.decryptAndHash(buffer)
|
||||
currentPattern += 1
|
||||
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
|
||||
private func performDHOperation(_ pattern: NoiseMessagePattern) throws {
|
||||
switch pattern {
|
||||
case .ee:
|
||||
@@ -712,7 +728,7 @@ final class NoiseHandshakeState {
|
||||
}
|
||||
let shared = try localEphemeral.sharedSecretFromKeyAgreement(with: remoteEphemeral)
|
||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
||||
|
||||
|
||||
case .es:
|
||||
if role == .initiator {
|
||||
guard let localEphemeral = localEphemeralPrivate,
|
||||
@@ -735,7 +751,7 @@ final class NoiseHandshakeState {
|
||||
// Clear sensitive shared secret
|
||||
keychain.secureClear(&sharedData)
|
||||
}
|
||||
|
||||
|
||||
case .se:
|
||||
if role == .initiator {
|
||||
guard let localStatic = localStaticPrivate,
|
||||
@@ -758,7 +774,7 @@ final class NoiseHandshakeState {
|
||||
// Clear sensitive shared secret
|
||||
keychain.secureClear(&sharedData)
|
||||
}
|
||||
|
||||
|
||||
case .ss:
|
||||
guard let localStatic = localStaticPrivate,
|
||||
let remoteStatic = remoteStaticPublic else {
|
||||
@@ -766,32 +782,32 @@ final class NoiseHandshakeState {
|
||||
}
|
||||
let shared = try localStatic.sharedSecretFromKeyAgreement(with: remoteStatic)
|
||||
symmetricState.mixKey(shared.withUnsafeBytes { Data($0) })
|
||||
|
||||
|
||||
case .e, .s:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func isHandshakeComplete() -> Bool {
|
||||
return currentPattern >= messagePatterns.count
|
||||
}
|
||||
|
||||
|
||||
func getTransportCiphers() throws -> (send: NoiseCipherState, receive: NoiseCipherState) {
|
||||
guard isHandshakeComplete() else {
|
||||
throw NoiseError.handshakeNotComplete
|
||||
}
|
||||
|
||||
|
||||
let (c1, c2) = symmetricState.split()
|
||||
|
||||
|
||||
// Initiator uses c1 for sending, c2 for receiving
|
||||
// Responder uses c2 for sending, c1 for receiving
|
||||
return role == .initiator ? (c1, c2) : (c2, c1)
|
||||
}
|
||||
|
||||
|
||||
func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? {
|
||||
return remoteStaticPublic
|
||||
}
|
||||
|
||||
|
||||
func getHandshakeHash() -> Data {
|
||||
return symmetricState.getHandshakeHash()
|
||||
}
|
||||
@@ -807,7 +823,7 @@ extension NoisePattern {
|
||||
case .NK: return "NK"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var messagePatterns: [[NoiseMessagePattern]] {
|
||||
switch self {
|
||||
case .XX:
|
||||
@@ -856,12 +872,12 @@ extension NoiseHandshakeState {
|
||||
guard keyData.count == 32 else {
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
|
||||
// Check for all-zero key (point at infinity)
|
||||
if keyData.allSatisfy({ $0 == 0 }) {
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
|
||||
// Check for low-order points that could enable small subgroup attacks
|
||||
// These are the known bad points for Curve25519
|
||||
let lowOrderPoints: [Data] = [
|
||||
@@ -882,13 +898,13 @@ extension NoiseHandshakeState {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) // Another bad point
|
||||
]
|
||||
|
||||
|
||||
// Check against known bad points
|
||||
if lowOrderPoints.contains(keyData) {
|
||||
SecureLogger.warning("Low-order point detected", category: .security)
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
|
||||
// Try to create the key - CryptoKit will validate curve points internally
|
||||
do {
|
||||
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData)
|
||||
|
||||
Reference in New Issue
Block a user