From b956e407ff0e0447f6cfbe76eedb443ba5ea385a Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 24 Nov 2025 10:43:19 -1000 Subject: [PATCH] Add unit tests for XChaCha20Poly1305Compat error handling 15 tests covering: - Valid input roundtrip (seal/open with and without AAD) - Different nonces produce different ciphertext - Invalid key length throws error (short, long, empty) - Invalid nonce length throws error (short, long, empty) - Error details contain expected/got values - Authentication fails with wrong key - Authentication fails with tampered ciphertext --- .../XChaCha20Poly1305CompatTests.swift | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 bitchatTests/XChaCha20Poly1305CompatTests.swift diff --git a/bitchatTests/XChaCha20Poly1305CompatTests.swift b/bitchatTests/XChaCha20Poly1305CompatTests.swift new file mode 100644 index 00000000..1bf0dd1b --- /dev/null +++ b/bitchatTests/XChaCha20Poly1305CompatTests.swift @@ -0,0 +1,231 @@ +// +// XChaCha20Poly1305CompatTests.swift +// bitchatTests +// +// Tests for XChaCha20-Poly1305 encryption with proper error handling. +// This is free and unencumbered software released into the public domain. +// + +import Testing +import Foundation +@testable import bitchat + +struct XChaCha20Poly1305CompatTests { + + // MARK: - Valid Input Tests + + @Test func sealAndOpenRoundtrip() throws { + let plaintext = "Hello, XChaCha20-Poly1305!".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) // 32-byte key + let nonce = Data(repeating: 0x24, count: 24) // 24-byte nonce + + let sealed = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: nonce) + let decrypted = try XChaCha20Poly1305Compat.open( + ciphertext: sealed.ciphertext, + tag: sealed.tag, + key: key, + nonce24: nonce + ) + + #expect(decrypted == plaintext) + } + + @Test func sealAndOpenWithAAD() throws { + let plaintext = "Secret message".data(using: .utf8)! + let key = Data(repeating: 0xAB, count: 32) + let nonce = Data(repeating: 0xCD, count: 24) + let aad = "additional authenticated data".data(using: .utf8)! + + let sealed = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: nonce, aad: aad) + let decrypted = try XChaCha20Poly1305Compat.open( + ciphertext: sealed.ciphertext, + tag: sealed.tag, + key: key, + nonce24: nonce, + aad: aad + ) + + #expect(decrypted == plaintext) + } + + @Test func sealProducesDifferentCiphertextWithDifferentNonces() throws { + let plaintext = "Same plaintext".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) + let nonce1 = Data(repeating: 0x01, count: 24) + let nonce2 = Data(repeating: 0x02, count: 24) + + let sealed1 = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: nonce1) + let sealed2 = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: nonce2) + + #expect(sealed1.ciphertext != sealed2.ciphertext) + } + + // MARK: - Invalid Key Length Tests + + @Test func sealThrowsOnShortKey() throws { + let plaintext = "Test".data(using: .utf8)! + let shortKey = Data(repeating: 0x42, count: 16) // Only 16 bytes, need 32 + let nonce = Data(repeating: 0x24, count: 24) + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: shortKey, nonce24: nonce) + } + } + + @Test func sealThrowsOnLongKey() throws { + let plaintext = "Test".data(using: .utf8)! + let longKey = Data(repeating: 0x42, count: 64) // 64 bytes, need 32 + let nonce = Data(repeating: 0x24, count: 24) + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: longKey, nonce24: nonce) + } + } + + @Test func sealThrowsOnEmptyKey() throws { + let plaintext = "Test".data(using: .utf8)! + let emptyKey = Data() + let nonce = Data(repeating: 0x24, count: 24) + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: emptyKey, nonce24: nonce) + } + } + + @Test func openThrowsOnInvalidKeyLength() throws { + let ciphertext = Data(repeating: 0x00, count: 16) + let tag = Data(repeating: 0x00, count: 16) + let shortKey = Data(repeating: 0x42, count: 31) // 31 bytes, need 32 + let nonce = Data(repeating: 0x24, count: 24) + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.open(ciphertext: ciphertext, tag: tag, key: shortKey, nonce24: nonce) + } + } + + // MARK: - Invalid Nonce Length Tests + + @Test func sealThrowsOnShortNonce() throws { + let plaintext = "Test".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) + let shortNonce = Data(repeating: 0x24, count: 12) // Only 12 bytes, need 24 + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: shortNonce) + } + } + + @Test func sealThrowsOnLongNonce() throws { + let plaintext = "Test".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) + let longNonce = Data(repeating: 0x24, count: 32) // 32 bytes, need 24 + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: longNonce) + } + } + + @Test func sealThrowsOnEmptyNonce() throws { + let plaintext = "Test".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) + let emptyNonce = Data() + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: emptyNonce) + } + } + + @Test func openThrowsOnInvalidNonceLength() throws { + let ciphertext = Data(repeating: 0x00, count: 16) + let tag = Data(repeating: 0x00, count: 16) + let key = Data(repeating: 0x42, count: 32) + let shortNonce = Data(repeating: 0x24, count: 23) // 23 bytes, need 24 + + #expect(throws: XChaCha20Poly1305Compat.Error.self) { + _ = try XChaCha20Poly1305Compat.open(ciphertext: ciphertext, tag: tag, key: key, nonce24: shortNonce) + } + } + + // MARK: - Error Detail Tests + + @Test func invalidKeyLengthErrorContainsDetails() throws { + let plaintext = "Test".data(using: .utf8)! + let badKey = Data(repeating: 0x42, count: 16) + let nonce = Data(repeating: 0x24, count: 24) + + do { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: badKey, nonce24: nonce) + Issue.record("Expected error to be thrown") + } catch let error as XChaCha20Poly1305Compat.Error { + if case .invalidKeyLength(let expected, let got) = error { + #expect(expected == 32) + #expect(got == 16) + } else { + Issue.record("Wrong error type") + } + } + } + + @Test func invalidNonceLengthErrorContainsDetails() throws { + let plaintext = "Test".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) + let badNonce = Data(repeating: 0x24, count: 12) + + do { + _ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: badNonce) + Issue.record("Expected error to be thrown") + } catch let error as XChaCha20Poly1305Compat.Error { + if case .invalidNonceLength(let expected, let got) = error { + #expect(expected == 24) + #expect(got == 12) + } else { + Issue.record("Wrong error type") + } + } + } + + // MARK: - Authentication Tests + + @Test func openFailsWithWrongKey() throws { + let plaintext = "Secret".data(using: .utf8)! + let correctKey = Data(repeating: 0x42, count: 32) + let wrongKey = Data(repeating: 0x43, count: 32) + let nonce = Data(repeating: 0x24, count: 24) + + let sealed = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: correctKey, nonce24: nonce) + + // Should throw authentication error (from ChaChaPoly) + #expect(throws: (any Error).self) { + _ = try XChaCha20Poly1305Compat.open( + ciphertext: sealed.ciphertext, + tag: sealed.tag, + key: wrongKey, + nonce24: nonce + ) + } + } + + @Test func openFailsWithTamperedCiphertext() throws { + let plaintext = "Secret".data(using: .utf8)! + let key = Data(repeating: 0x42, count: 32) + let nonce = Data(repeating: 0x24, count: 24) + + let sealed = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: nonce) + + // Tamper with ciphertext + var tampered = sealed.ciphertext + if !tampered.isEmpty { + tampered[0] ^= 0xFF + } + + // Should throw authentication error + #expect(throws: (any Error).self) { + _ = try XChaCha20Poly1305Compat.open( + ciphertext: tampered, + tag: sealed.tag, + key: key, + nonce24: nonce + ) + } + } +}