Add ChaCha20-Poly1305 AEAD, scrypt KDF, and build fixes
ober
92262b574f643e61ae4c3cae54ea76152ab72ed5
--- a/.gitignore +++ b/.gitignore @@ -4,3 +4,12 @@ *~ \#* .#* + +# Rust build artifacts +jerboa-native-rs/target/ + +# Claude Code exports +new.txt + +# External project stubs (belong in their own repos) +lib/gerbil-litehtml/ --- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -47,6 +47,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -96,6 +102,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -289,6 +305,15 @@ dependencies = [ ] [[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] name = "jerboa-native" version = "0.1.0" dependencies = [ @@ -299,6 +324,7 @@ dependencies = [ "regex", "ring", "rusqlite", + "scrypt", ] [[package]] @@ -437,6 +463,27 @@ dependencies = [ ] [[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -556,7 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -566,7 +613,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -651,12 +707,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/jerboa-native-rs/Cargo.toml +++ b/jerboa-native-rs/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["cdylib", "staticlib"] [dependencies] ring = "0.17" +scrypt = "0.11" flate2 = "1" regex = "1" libc = "0.2" @@ -19,3 +20,4 @@ inotify = { version = "0.11", default-features = false } lto = true codegen-units = 1 strip = true +panic = "abort" --- a/jerboa-native-rs/src/crypto.rs +++ b/jerboa-native-rs/src/crypto.rs @@ -259,6 +259,145 @@ pub extern "C" fn jerboa_aead_open( }) } +// --- AEAD (ChaCha20-Poly1305) --- + +#[no_mangle] +pub extern "C" fn jerboa_chacha20_seal( + key: *const u8, key_len: usize, + nonce: *const u8, nonce_len: usize, + plaintext: *const u8, pt_len: usize, + aad: *const u8, aad_len: usize, + output: *mut u8, output_max: usize, + output_len: *mut usize, +) -> i32 { + ffi_wrap(|| { + if key.is_null() || nonce.is_null() || output.is_null() || output_len.is_null() { + return -1; + } + let needed = pt_len + aead::CHACHA20_POLY1305.tag_len(); + if output_max < needed { return -1; } + if nonce_len != 12 { return -1; } + if key_len != 32 { return -1; } + + let k = unsafe { std::slice::from_raw_parts(key, key_len) }; + let n = unsafe { std::slice::from_raw_parts(nonce, nonce_len) }; + let pt = if pt_len == 0 { &[] } else { + unsafe { std::slice::from_raw_parts(plaintext, pt_len) } + }; + let ad = if aad_len == 0 || aad.is_null() { &[] } else { + unsafe { std::slice::from_raw_parts(aad, aad_len) } + }; + + let unbound_key = match aead::UnboundKey::new(&aead::CHACHA20_POLY1305, k) { + Ok(uk) => uk, + Err(_) => return -1, + }; + let sealing_key = aead::LessSafeKey::new(unbound_key); + let nonce_val = match aead::Nonce::try_assume_unique_for_key(n) { + Ok(nv) => nv, + Err(_) => return -1, + }; + + let out = unsafe { std::slice::from_raw_parts_mut(output, output_max) }; + out[..pt_len].copy_from_slice(pt); + + let aad_obj = aead::Aad::from(ad); + match sealing_key.seal_in_place_separate_tag(nonce_val, aad_obj, &mut out[..pt_len]) { + Ok(tag) => { + out[pt_len..pt_len + tag.as_ref().len()].copy_from_slice(tag.as_ref()); + unsafe { *output_len = needed; } + 0 + } + Err(_) => -1, + } + }) +} + +#[no_mangle] +pub extern "C" fn jerboa_chacha20_open( + key: *const u8, key_len: usize, + nonce: *const u8, nonce_len: usize, + ciphertext: *const u8, ct_len: usize, + aad: *const u8, aad_len: usize, + output: *mut u8, output_max: usize, + output_len: *mut usize, +) -> i32 { + ffi_wrap(|| { + if key.is_null() || nonce.is_null() || output.is_null() || output_len.is_null() { + return -1; + } + let tag_len = aead::CHACHA20_POLY1305.tag_len(); + if ct_len < tag_len { return -1; } + let pt_len = ct_len - tag_len; + if output_max < pt_len { return -1; } + if nonce_len != 12 { return -1; } + if key_len != 32 { return -1; } + + let k = unsafe { std::slice::from_raw_parts(key, key_len) }; + let n = unsafe { std::slice::from_raw_parts(nonce, nonce_len) }; + let ct = unsafe { std::slice::from_raw_parts(ciphertext, ct_len) }; + let ad = if aad_len == 0 || aad.is_null() { &[] } else { + unsafe { std::slice::from_raw_parts(aad, aad_len) } + }; + + let unbound_key = match aead::UnboundKey::new(&aead::CHACHA20_POLY1305, k) { + Ok(uk) => uk, + Err(_) => return -1, + }; + let opening_key = aead::LessSafeKey::new(unbound_key); + let nonce_val = match aead::Nonce::try_assume_unique_for_key(n) { + Ok(nv) => nv, + Err(_) => return -1, + }; + + let out = unsafe { std::slice::from_raw_parts_mut(output, output_max.max(ct_len)) }; + out[..ct_len].copy_from_slice(ct); + + let aad_obj = aead::Aad::from(ad); + match opening_key.open_in_place(nonce_val, aad_obj, &mut out[..ct_len]) { + Ok(plaintext) => { + let plen = plaintext.len(); + unsafe { *output_len = plen; } + 0 + } + Err(_) => -1, + } + }) +} + +// --- Scrypt KDF --- + +#[no_mangle] +pub extern "C" fn jerboa_scrypt( + password: *const u8, password_len: usize, + salt: *const u8, salt_len: usize, + log_n: u8, r: u32, p: u32, + output: *mut u8, output_len: usize, +) -> i32 { + ffi_wrap(|| { + if password.is_null() || salt.is_null() || output.is_null() { return -1; } + if output_len == 0 { return -1; } + let pw = unsafe { std::slice::from_raw_parts(password, password_len) }; + let s = unsafe { std::slice::from_raw_parts(salt, salt_len) }; + let out = unsafe { std::slice::from_raw_parts_mut(output, output_len) }; + + let params = match scrypt::Params::new(log_n, r, p, output_len) { + Ok(p) => p, + Err(_) => { + set_last_error("invalid scrypt parameters".to_string()); + return -1; + } + }; + match scrypt::scrypt(pw, s, ¶ms, out) { + Ok(()) => 0, + Err(_) => { + set_last_error("scrypt derivation failed".to_string()); + -1 + } + } + }) +} + // --- PBKDF2 --- #[no_mangle] --- a/lib/std/actor/transport.sls +++ b/lib/std/actor/transport.sls @@ -214,7 +214,7 @@ (unless (native-crypto-memcmp server-proof expected) (tcp-close fd) (error 'open-connection! "server auth failed — possible MITM" node-id)) - (vector fd write-mutex)))))))))) + (vector fd write-mutex))))))))) ;; -------- 7E: Remote send -------- @@ -287,7 +287,7 @@ (unless (eq? msg 'eof) (dispatch-remote-message! msg) (loop)))) - (tcp-close fd)))))))))))) + (tcp-close fd))))))))))) ;; Dispatch an inbound message to a local actor. ;; Expected wire format: (send local-actor-id payload) --- a/lib/std/crypto/native-rust.sls +++ b/lib/std/crypto/native-rust.sls @@ -14,8 +14,12 @@ rust-hmac-sha256 rust-hmac-sha256-verify ;; Timing-safe comparison rust-timing-safe-equal? - ;; AEAD + ;; AEAD (AES-256-GCM) rust-aead-seal rust-aead-open + ;; AEAD (ChaCha20-Poly1305) + rust-chacha20-seal rust-chacha20-open + ;; Scrypt KDF + rust-scrypt ;; PBKDF2 rust-pbkdf2-derive rust-pbkdf2-verify ;; Error @@ -23,11 +27,12 @@ (import (chezscheme)) - ;; Load the Rust native library + ;; Load the Rust native library (for dynamic builds). + ;; In static builds, symbols are pre-registered via Sforeign_symbol — skip loading. (define _native-loaded (or (guard (e [#t #f]) (load-shared-object "libjerboa_native.so") #t) (guard (e [#t #f]) (load-shared-object "lib/libjerboa_native.so") #t) - (error 'std/crypto/native-rust "libjerboa_native.so not found"))) + #t)) ;; Helper: extract sub-bytevector (avoids Chez extension warning) (define (bv-sub bv start len) @@ -169,6 +174,70 @@ (let ([actual-len (bytevector-u64-native-ref len-buf 0)]) (bv-sub out 0 actual-len))))) + ;; --- AEAD (ChaCha20-Poly1305) --- + + (define c-jerboa-chacha20-seal + (foreign-procedure "jerboa_chacha20_seal" + (u8* size_t u8* size_t u8* size_t u8* size_t u8* size_t u8*) int)) + + ;; Encrypt with ChaCha20-Poly1305. Returns ciphertext||tag bytevector. + (define (rust-chacha20-seal key nonce plaintext aad) + (let* ([pt-len (bytevector-length plaintext)] + [out-max (+ pt-len 16)] + [out (make-bytevector out-max)] + [len-buf (make-bytevector 8)]) + (let ([rc (c-jerboa-chacha20-seal key (bytevector-length key) + nonce (bytevector-length nonce) + plaintext pt-len + aad (bytevector-length aad) + out out-max + len-buf)]) + (when (< rc 0) (error 'rust-chacha20-seal "seal failed" (rust-last-error))) + (let ([actual-len (bytevector-u64-native-ref len-buf 0)]) + (if (= actual-len out-max) + out + (bv-sub out 0 actual-len)))))) + + (define c-jerboa-chacha20-open + (foreign-procedure "jerboa_chacha20_open" + (u8* size_t u8* size_t u8* size_t u8* size_t u8* size_t u8*) int)) + + ;; Decrypt with ChaCha20-Poly1305. Returns plaintext or raises error. + (define (rust-chacha20-open key nonce ciphertext aad) + (let* ([ct-len (bytevector-length ciphertext)] + [out-max ct-len] + [out (make-bytevector out-max)] + [len-buf (make-bytevector 8)]) + (let ([rc (c-jerboa-chacha20-open key (bytevector-length key) + nonce (bytevector-length nonce) + ciphertext ct-len + aad (bytevector-length aad) + out out-max + len-buf)]) + (when (< rc 0) (error 'rust-chacha20-open "open failed" (rust-last-error))) + (let ([actual-len (bytevector-u64-native-ref len-buf 0)]) + (bv-sub out 0 actual-len))))) + + ;; --- Scrypt KDF --- + + (define c-jerboa-scrypt + (foreign-procedure "jerboa_scrypt" + (u8* size_t u8* size_t unsigned-8 unsigned-32 unsigned-32 u8* size_t) int)) + + ;; Derive key using scrypt. Takes N (power of 2, e.g. 16384), r, p. + ;; Converts N to log2(N) for the Rust API. + (define (rust-scrypt password salt output-len n r p) + (let* ([pw (if (string? password) (string->utf8 password) password)] + [s (if (string? password) (string->utf8 salt) salt)] + [log-n (bitwise-length (- n 1))] ;; log2(16384) = 14 + [out (make-bytevector output-len)]) + (let ([rc (c-jerboa-scrypt pw (bytevector-length pw) + s (bytevector-length s) + log-n r p + out output-len)]) + (when (< rc 0) (error 'rust-scrypt "scrypt failed" (rust-last-error))) + out))) + ;; --- PBKDF2 --- (define c-jerboa-pbkdf2-derive --- a/lib/std/net/tcp-raw.sls +++ b/lib/std/net/tcp-raw.sls @@ -15,6 +15,13 @@ ;; ========== FFI ========== + ;; Load libc so POSIX socket functions are available at compile time. + ;; Without this, compile-library fails because foreign-procedure can't + ;; resolve "socket" etc. during ahead-of-time compilation. + (define _libc + (guard (exn [#t (void)]) + (load-shared-object "libc.so.6"))) + (define c-socket (foreign-procedure "socket" (int int int) int)) (define c-bind (foreign-procedure "bind" (int void* int) int)) (define c-listen (foreign-procedure "listen" (int int) int))