mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 04:05:20 +00:00
Merge pull request #888 from permissionlesstech/fix/crypto-precondition-crashes
Fix: Replace precondition() crashes with throwing errors in crypto code
This commit is contained in:
@@ -5,16 +5,27 @@ import CryptoKit
|
|||||||
/// Implements HChaCha20 to derive a subkey and reduces the 24-byte nonce to a 12-byte nonce
|
/// Implements HChaCha20 to derive a subkey and reduces the 24-byte nonce to a 12-byte nonce
|
||||||
/// as per XChaCha20 construction.
|
/// as per XChaCha20 construction.
|
||||||
enum XChaCha20Poly1305Compat {
|
enum XChaCha20Poly1305Compat {
|
||||||
|
|
||||||
|
/// Errors that can occur during XChaCha20-Poly1305 operations
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case invalidKeyLength(expected: Int, got: Int)
|
||||||
|
case invalidNonceLength(expected: Int, got: Int)
|
||||||
|
}
|
||||||
|
|
||||||
struct SealBox {
|
struct SealBox {
|
||||||
let ciphertext: Data
|
let ciphertext: Data
|
||||||
let tag: Data
|
let tag: Data
|
||||||
}
|
}
|
||||||
|
|
||||||
static func seal(plaintext: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> SealBox {
|
static func seal(plaintext: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> SealBox {
|
||||||
precondition(key.count == 32, "XChaCha20 key must be 32 bytes")
|
guard key.count == 32 else {
|
||||||
precondition(nonce24.count == 24, "XChaCha20 nonce must be 24 bytes")
|
throw Error.invalidKeyLength(expected: 32, got: key.count)
|
||||||
|
}
|
||||||
|
guard nonce24.count == 24 else {
|
||||||
|
throw Error.invalidNonceLength(expected: 24, got: nonce24.count)
|
||||||
|
}
|
||||||
|
|
||||||
let subkey = hchacha20(key: key, nonce16: nonce24.prefix(16))
|
let subkey = try hchacha20(key: key, nonce16: Data(nonce24.prefix(16)))
|
||||||
let nonce12 = derive12ByteNonce(from24: nonce24)
|
let nonce12 = derive12ByteNonce(from24: nonce24)
|
||||||
let chachaKey = SymmetricKey(data: subkey)
|
let chachaKey = SymmetricKey(data: subkey)
|
||||||
let nonce = try ChaChaPoly.Nonce(data: nonce12)
|
let nonce = try ChaChaPoly.Nonce(data: nonce12)
|
||||||
@@ -23,10 +34,14 @@ enum XChaCha20Poly1305Compat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func open(ciphertext: Data, tag: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> Data {
|
static func open(ciphertext: Data, tag: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> Data {
|
||||||
precondition(key.count == 32, "XChaCha20 key must be 32 bytes")
|
guard key.count == 32 else {
|
||||||
precondition(nonce24.count == 24, "XChaCha20 nonce must be 24 bytes")
|
throw Error.invalidKeyLength(expected: 32, got: key.count)
|
||||||
|
}
|
||||||
|
guard nonce24.count == 24 else {
|
||||||
|
throw Error.invalidNonceLength(expected: 24, got: nonce24.count)
|
||||||
|
}
|
||||||
|
|
||||||
let subkey = hchacha20(key: key, nonce16: nonce24.prefix(16))
|
let subkey = try hchacha20(key: key, nonce16: Data(nonce24.prefix(16)))
|
||||||
let nonce12 = derive12ByteNonce(from24: nonce24)
|
let nonce12 = derive12ByteNonce(from24: nonce24)
|
||||||
let chachaKey = SymmetricKey(data: subkey)
|
let chachaKey = SymmetricKey(data: subkey)
|
||||||
let box = try ChaChaPoly.SealedBox(nonce: ChaChaPoly.Nonce(data: nonce12), ciphertext: ciphertext, tag: tag)
|
let box = try ChaChaPoly.SealedBox(nonce: ChaChaPoly.Nonce(data: nonce12), ciphertext: ciphertext, tag: tag)
|
||||||
@@ -43,10 +58,14 @@ enum XChaCha20Poly1305Compat {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func hchacha20(key: Data, nonce16: Data) -> Data {
|
private static func hchacha20(key: Data, nonce16: Data) throws -> Data {
|
||||||
// HChaCha20 based on the original ChaCha20 core with a 16-byte nonce.
|
// HChaCha20 based on the original ChaCha20 core with a 16-byte nonce.
|
||||||
precondition(key.count == 32)
|
guard key.count == 32 else {
|
||||||
precondition(nonce16.count == 16)
|
throw Error.invalidKeyLength(expected: 32, got: key.count)
|
||||||
|
}
|
||||||
|
guard nonce16.count == 16 else {
|
||||||
|
throw Error.invalidNonceLength(expected: 16, got: nonce16.count)
|
||||||
|
}
|
||||||
|
|
||||||
// Constants "expand 32-byte k"
|
// Constants "expand 32-byte k"
|
||||||
var state: [UInt32] = [
|
var state: [UInt32] = [
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
//
|
||||||
|
// 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 struct Foundation.Data
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
struct XChaCha20Poly1305CompatTests {
|
||||||
|
|
||||||
|
@Test func sealAndOpenRoundtrip() throws {
|
||||||
|
let plaintext = "Hello, XChaCha20-Poly1305!".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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sealThrowsOnShortKey() {
|
||||||
|
let plaintext = "Test".data(using: .utf8)!
|
||||||
|
let shortKey = Data(repeating: 0x42, count: 16)
|
||||||
|
let nonce = Data(repeating: 0x24, count: 24)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: shortKey, nonce24: nonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sealThrowsOnLongKey() {
|
||||||
|
let plaintext = "Test".data(using: .utf8)!
|
||||||
|
let longKey = Data(repeating: 0x42, count: 64)
|
||||||
|
let nonce = Data(repeating: 0x24, count: 24)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: longKey, nonce24: nonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sealThrowsOnEmptyKey() {
|
||||||
|
let plaintext = "Test".data(using: .utf8)!
|
||||||
|
let emptyKey = Data()
|
||||||
|
let nonce = Data(repeating: 0x24, count: 24)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: emptyKey, nonce24: nonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func openThrowsOnInvalidKeyLength() {
|
||||||
|
let ciphertext = Data(repeating: 0x00, count: 16)
|
||||||
|
let tag = Data(repeating: 0x00, count: 16)
|
||||||
|
let shortKey = Data(repeating: 0x42, count: 31)
|
||||||
|
let nonce = Data(repeating: 0x24, count: 24)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.open(ciphertext: ciphertext, tag: tag, key: shortKey, nonce24: nonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sealThrowsOnShortNonce() {
|
||||||
|
let plaintext = "Test".data(using: .utf8)!
|
||||||
|
let key = Data(repeating: 0x42, count: 32)
|
||||||
|
let shortNonce = Data(repeating: 0x24, count: 12)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: shortNonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sealThrowsOnLongNonce() {
|
||||||
|
let plaintext = "Test".data(using: .utf8)!
|
||||||
|
let key = Data(repeating: 0x42, count: 32)
|
||||||
|
let longNonce = Data(repeating: 0x24, count: 32)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: longNonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sealThrowsOnEmptyNonce() {
|
||||||
|
let plaintext = "Test".data(using: .utf8)!
|
||||||
|
let key = Data(repeating: 0x42, count: 32)
|
||||||
|
let emptyNonce = Data()
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.seal(plaintext: plaintext, key: key, nonce24: emptyNonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func openThrowsOnInvalidNonceLength() {
|
||||||
|
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)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.open(ciphertext: ciphertext, tag: tag, key: key, nonce24: shortNonce)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.open(
|
||||||
|
ciphertext: sealed.ciphertext,
|
||||||
|
tag: sealed.tag,
|
||||||
|
key: wrongKey,
|
||||||
|
nonce24: nonce
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
// Create tampered ciphertext by changing first byte
|
||||||
|
var tamperedBytes = [UInt8](sealed.ciphertext)
|
||||||
|
tamperedBytes[0] = tamperedBytes[0] ^ 0xFF
|
||||||
|
let tampered = Data(tamperedBytes)
|
||||||
|
|
||||||
|
var didThrow = false
|
||||||
|
do {
|
||||||
|
_ = try XChaCha20Poly1305Compat.open(
|
||||||
|
ciphertext: tampered,
|
||||||
|
tag: sealed.tag,
|
||||||
|
key: key,
|
||||||
|
nonce24: nonce
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
didThrow = true
|
||||||
|
}
|
||||||
|
#expect(didThrow)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user