Files
bitchat/bitchatTests/XChaCha20Poly1305CompatTests.swift
T
jack b956e407ff 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
2025-11-24 10:43:19 -10:00

232 lines
8.4 KiB
Swift

//
// 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
)
}
}
}