mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 13:25:20 +00:00
Mechanical style fixes across the enabled rule set, mostly via swiftlint --fix (trailing_comma, comma, colon, trailing_newline, comment_spacing, unused_closure_parameter, unneeded_break_in_switch, opening_brace) plus hand fixes: - non_optional_string_data_conversion (45): .data(using: .utf8)! and ?? Data() fallbacks replaced with the non-optional Data(_.utf8), including two production sites (NIP-44 HKDF info constant and the announce canonicalization context/nickname bytes — byte-identical output, only the impossible-nil handling is gone). - switch_case_alignment: LocationChannel had a misindented closing brace; also repaired an --fix artifact in BLEService's .none case. - redundant_string_enum_value: TrustLevel raw values equal to the case names (encoded form unchanged). - unused_optional_binding: let _ = binds replaced with != nil / is Bool. - static_over_final_class: PreviewView.layerClass. - Resolved the BinaryProtocolTests TODO by documenting that 8-byte recipient ID truncation is the fixed wire-field size, not a bug. The 4 remaining violations are all todo markers for a shared test-helpers module (tracked in #1088) and one Reuse note. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
135 lines
5.3 KiB
Swift
135 lines
5.3 KiB
Swift
import Foundation
|
|
import CryptoKit
|
|
|
|
/// Minimal XChaCha20-Poly1305 compatibility wrapper using CryptoKit's ChaChaPoly.
|
|
/// Implements HChaCha20 to derive a subkey and reduces the 24-byte nonce to a 12-byte nonce
|
|
/// as per XChaCha20 construction.
|
|
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 {
|
|
let ciphertext: Data
|
|
let tag: Data
|
|
}
|
|
|
|
static func seal(plaintext: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> SealBox {
|
|
guard key.count == 32 else {
|
|
throw Error.invalidKeyLength(expected: 32, got: key.count)
|
|
}
|
|
guard nonce24.count == 24 else {
|
|
throw Error.invalidNonceLength(expected: 24, got: nonce24.count)
|
|
}
|
|
|
|
let subkey = try hchacha20(key: key, nonce16: Data(nonce24.prefix(16)))
|
|
let nonce12 = derive12ByteNonce(from24: nonce24)
|
|
let chachaKey = SymmetricKey(data: subkey)
|
|
let nonce = try ChaChaPoly.Nonce(data: nonce12)
|
|
let sealed = try ChaChaPoly.seal(plaintext, using: chachaKey, nonce: nonce, authenticating: aad ?? Data())
|
|
return SealBox(ciphertext: sealed.ciphertext, tag: sealed.tag)
|
|
}
|
|
|
|
static func open(ciphertext: Data, tag: Data, key: Data, nonce24: Data, aad: Data? = nil) throws -> Data {
|
|
guard key.count == 32 else {
|
|
throw Error.invalidKeyLength(expected: 32, got: key.count)
|
|
}
|
|
guard nonce24.count == 24 else {
|
|
throw Error.invalidNonceLength(expected: 24, got: nonce24.count)
|
|
}
|
|
|
|
let subkey = try hchacha20(key: key, nonce16: Data(nonce24.prefix(16)))
|
|
let nonce12 = derive12ByteNonce(from24: nonce24)
|
|
let chachaKey = SymmetricKey(data: subkey)
|
|
let box = try ChaChaPoly.SealedBox(nonce: ChaChaPoly.Nonce(data: nonce12), ciphertext: ciphertext, tag: tag)
|
|
return try ChaChaPoly.open(box, using: chachaKey, authenticating: aad ?? Data())
|
|
}
|
|
|
|
// MARK: - Internals
|
|
|
|
private static func derive12ByteNonce(from24 nonce24: Data) -> Data {
|
|
// XChaCha20-Poly1305: 12-byte nonce = 4 zero bytes || last 8 bytes of the 24-byte nonce
|
|
var out = Data(count: 12)
|
|
out.replaceSubrange(0..<4, with: [0, 0, 0, 0])
|
|
out.replaceSubrange(4..<12, with: nonce24.suffix(8))
|
|
return out
|
|
}
|
|
|
|
private static func hchacha20(key: Data, nonce16: Data) throws -> Data {
|
|
// HChaCha20 based on the original ChaCha20 core with a 16-byte nonce.
|
|
guard key.count == 32 else {
|
|
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"
|
|
var state: [UInt32] = [
|
|
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574,
|
|
// key (8 words)
|
|
key.loadLEWord(0), key.loadLEWord(4), key.loadLEWord(8), key.loadLEWord(12),
|
|
key.loadLEWord(16), key.loadLEWord(20), key.loadLEWord(24), key.loadLEWord(28),
|
|
// nonce (4 words)
|
|
nonce16.loadLEWord(0), nonce16.loadLEWord(4), nonce16.loadLEWord(8), nonce16.loadLEWord(12)
|
|
]
|
|
|
|
// 20 rounds (10 double rounds)
|
|
for _ in 0..<10 {
|
|
// Column rounds
|
|
quarterRound(&state, 0, 4, 8, 12)
|
|
quarterRound(&state, 1, 5, 9, 13)
|
|
quarterRound(&state, 2, 6, 10, 14)
|
|
quarterRound(&state, 3, 7, 11, 15)
|
|
// Diagonal rounds
|
|
quarterRound(&state, 0, 5, 10, 15)
|
|
quarterRound(&state, 1, 6, 11, 12)
|
|
quarterRound(&state, 2, 7, 8, 13)
|
|
quarterRound(&state, 3, 4, 9, 14)
|
|
}
|
|
|
|
// Output subkey: state[0..3] and state[12..15]
|
|
var out = Data(count: 32)
|
|
out.storeLEWord(state[0], at: 0)
|
|
out.storeLEWord(state[1], at: 4)
|
|
out.storeLEWord(state[2], at: 8)
|
|
out.storeLEWord(state[3], at: 12)
|
|
out.storeLEWord(state[12], at: 16)
|
|
out.storeLEWord(state[13], at: 20)
|
|
out.storeLEWord(state[14], at: 24)
|
|
out.storeLEWord(state[15], at: 28)
|
|
return out
|
|
}
|
|
|
|
private static func quarterRound(_ s: inout [UInt32], _ a: Int, _ b: Int, _ c: Int, _ d: Int) {
|
|
s[a] = s[a] &+ s[b]; s[d] ^= s[a]; s[d] = (s[d] << 16) | (s[d] >> 16)
|
|
s[c] = s[c] &+ s[d]; s[b] ^= s[c]; s[b] = (s[b] << 12) | (s[b] >> 20)
|
|
s[a] = s[a] &+ s[b]; s[d] ^= s[a]; s[d] = (s[d] << 8) | (s[d] >> 24)
|
|
s[c] = s[c] &+ s[d]; s[b] ^= s[c]; s[b] = (s[b] << 7) | (s[b] >> 25)
|
|
}
|
|
}
|
|
|
|
private extension Data {
|
|
func loadLEWord(_ offset: Int) -> UInt32 {
|
|
let range = offset..<(offset+4)
|
|
let bytes = self[range]
|
|
return bytes.withUnsafeBytes { ptr -> UInt32 in
|
|
let b = ptr.bindMemory(to: UInt8.self)
|
|
return UInt32(b[0]) | (UInt32(b[1]) << 8) | (UInt32(b[2]) << 16) | (UInt32(b[3]) << 24)
|
|
}
|
|
}
|
|
|
|
mutating func storeLEWord(_ value: UInt32, at offset: Int) {
|
|
let bytes: [UInt8] = [
|
|
UInt8(value & 0xff),
|
|
UInt8((value >> 8) & 0xff),
|
|
UInt8((value >> 16) & 0xff),
|
|
UInt8((value >> 24) & 0xff)
|
|
]
|
|
replaceSubrange(offset..<(offset+4), with: bytes)
|
|
}
|
|
}
|