From 89b8087a37fa2130ef69022f36aedc2324bc849d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 00:55:13 +0000 Subject: [PATCH 1/3] ogar-encryption: own the wasm bindings on the argon2-0.6 envelope The `wasm` feature used to forward ndarray encryption's wasm module, whose seal/open ran that crate's argon2-0.5 envelope. src/wasm.rs now exports the same seven functions over this crate's envelope (plus the re-exported sign and hash). The encryption dep is default-features = false so, with ndarray #333, argon2 0.5 leaves the tree. Fixes a doctest that imported encryption::kdf. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm --- crates/ogar-encryption/Cargo.toml | 15 +++-- crates/ogar-encryption/src/kdf.rs | 2 +- crates/ogar-encryption/src/lib.rs | 11 ++-- crates/ogar-encryption/src/wasm.rs | 92 ++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 crates/ogar-encryption/src/wasm.rs diff --git a/crates/ogar-encryption/Cargo.toml b/crates/ogar-encryption/Cargo.toml index 4b9bb64..78e57f2 100644 --- a/crates/ogar-encryption/Cargo.toml +++ b/crates/ogar-encryption/Cargo.toml @@ -10,7 +10,9 @@ description = "OGAR's single, generic, classid-agnostic encryption surface: the # XChaCha20-Poly1305 AEAD, Ed25519 signatures and SHA-384 are REUSED from the # ndarray fork's `encryption` crate and re-exported verbatim (see lib.rs). The # Argon2 half (kdf + envelope) is owned here — see below. -encryption = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master" } +# `default-features = false` drops that crate's own argon2-0.5 KDF + envelope +# (its `kdf` feature, ndarray #333); this crate carries both on argon2 0.6. +encryption = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master", default-features = false } # Not in ndarray: `kdf` and `envelope` run here on argon2 0.6 # from the AdaWorldAPI/password-hashes fork. `ndarray-simd` puts the block @@ -20,11 +22,14 @@ argon2 = { git = "https://github.com/AdaWorldAPI/password-hashes", branch = "mas zeroize = { version = "1", features = ["derive"] } getrandom = "0.2" +# Optional browser bindings (src/wasm.rs), built on this crate's own envelope. +wasm-bindgen = { version = "0.2", optional = true } + [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"] } [features] -# Forward to the ndarray crate's wasm bindings, so browser consumers (e.g. a -# hub-client-style SPA that wants client-side `envelope::seal` before secrets -# ever reach a server) get the wasm-bindgen surface through this crate too. -wasm = ["encryption/wasm-bindings"] +# wasm-bindgen bindings, so browser consumers (e.g. a hub-client-style SPA that +# wants client-side `envelope::seal` before secrets ever reach a server) get +# seal/open, Ed25519 and SHA-384 from this crate. +wasm = ["dep:wasm-bindgen"] diff --git a/crates/ogar-encryption/src/kdf.rs b/crates/ogar-encryption/src/kdf.rs index a6519e5..7f0c0cc 100644 --- a/crates/ogar-encryption/src/kdf.rs +++ b/crates/ogar-encryption/src/kdf.rs @@ -97,7 +97,7 @@ impl KdfParams { /// Rejecting must therefore be *cheap* — a comparison, not an attempt. /// /// ``` - /// use encryption::kdf::{CostLimits, KdfParams, KdfError}; + /// use ogar_encryption::kdf::{CostLimits, KdfParams, KdfError}; /// /// assert!(KdfParams::INTERACTIVE.validate().is_ok()); /// diff --git a/crates/ogar-encryption/src/lib.rs b/crates/ogar-encryption/src/lib.rs index d5ec7fe..a82a064 100644 --- a/crates/ogar-encryption/src/lib.rs +++ b/crates/ogar-encryption/src/lib.rs @@ -23,7 +23,7 @@ //! | [`seal`], [`open`] | — | root-level aliases for `envelope::seal` / `envelope::open` | //! | [`EnvelopeError`], [`KdfParams`] | — | root-level aliases for the envelope's error + parameter types | //! | [`RngError`] | — | the platform-CSPRNG-unavailable error | -//! | [`wasm`] (feature `wasm`) | — | wasm-bindgen bindings for browser consumers | +//! | [`wasm`] (feature `wasm`, local) | — | wasm-bindgen bindings for browser consumers | //! //! ## Generic, classid-agnostic, no secrets — by construction //! @@ -77,10 +77,7 @@ pub(crate) fn fill_random(buf: &mut [u8]) -> Result<(), RngError> { getrandom::getrandom(buf).map_err(|_| RngError) } -/// wasm-bindgen bindings for browser consumers (forwarded from -/// [`encryption`]'s `wasm-bindings` feature via this crate's `wasm` feature). -/// -/// NOTE: these bindings still run [`encryption`]'s own envelope (argon2 0.5); -/// moving them here is a follow-up. +/// wasm-bindgen bindings for browser consumers (feature `wasm`): seal/open +/// on this crate's argon2-0.6 envelope, plus Ed25519 and SHA-384. #[cfg(feature = "wasm")] -pub use encryption::wasm; +pub mod wasm; diff --git a/crates/ogar-encryption/src/wasm.rs b/crates/ogar-encryption/src/wasm.rs new file mode 100644 index 0000000..5699870 --- /dev/null +++ b/crates/ogar-encryption/src/wasm.rs @@ -0,0 +1,92 @@ +//! Browser bindings (`--features wasm`, target wasm32). +//! +//! Thin `wasm-bindgen` façade over the crate's Rust API so a stock +//! browser can call it from JavaScript after a `wasm-pack build`: +//! +//! ```js +//! import init, { seal_envelope, open_envelope } from "./pkg/ogar_encryption.js"; +//! await init(); +//! const blob = seal_envelope(pwUtf8, secretBytes, true /* interactive */); +//! // ship `blob` to the server — it is unreadable there +//! const secret = open_envelope(pwUtf8, blob); +//! ``` +//! +//! Errors surface as thrown JS strings (the `Display` of the Rust +//! error) — deliberately message-only, never secret material. +//! +//! Ported from the ndarray `encryption` crate's `wasm` module with the +//! same exported functions; `seal_envelope` / `open_envelope` now run this +//! crate's argon2-0.6 [`crate::envelope`]. + +use wasm_bindgen::prelude::*; + +use crate::envelope::{self, KdfParams}; +use crate::sign::{Keypair, PUBLIC_KEY_LEN, SEED_LEN, SIGNATURE_LEN}; + +fn params(interactive: bool) -> KdfParams { + if interactive { + KdfParams::INTERACTIVE + } else { + KdfParams::DEFAULT + } +} + +/// Seal `plaintext` under `password` client-side. With +/// `interactive = true` the browser-grade Argon2id cost is used. +#[wasm_bindgen] +pub fn seal_envelope(password: &[u8], plaintext: &[u8], interactive: bool) -> Result, JsError> { + envelope::seal(password, plaintext, ¶ms(interactive)).map_err(|e| JsError::new(&e.to_string())) +} + +/// Open a sealed envelope. Throws on wrong password or tampering. +#[wasm_bindgen] +pub fn open_envelope(password: &[u8], blob: &[u8]) -> Result, JsError> { + envelope::open(password, blob).map_err(|e| JsError::new(&e.to_string())) +} + +/// Generate a fresh Ed25519 seed (32 bytes) from the browser CSPRNG. +/// The caller is responsible for storing it sealed (see +/// [`seal_envelope`]) — never in plaintext localStorage. +#[wasm_bindgen] +pub fn generate_signing_seed() -> Result, JsError> { + let mut seed = [0u8; SEED_LEN]; + crate::fill_random(&mut seed).map_err(|e| JsError::new(&e.to_string()))?; + Ok(seed.to_vec()) +} + +/// Derive the 32-byte public key for a seed. +#[wasm_bindgen] +pub fn public_key_of(seed: &[u8]) -> Result, JsError> { + let seed: [u8; SEED_LEN] = seed + .try_into() + .map_err(|_| JsError::new("seed must be 32 bytes"))?; + Ok(Keypair::from_seed(&seed).public_key().to_vec()) +} + +/// Sign `message` with `seed`; returns the 64-byte signature. +#[wasm_bindgen] +pub fn sign_message(seed: &[u8], message: &[u8]) -> Result, JsError> { + let seed: [u8; SEED_LEN] = seed + .try_into() + .map_err(|_| JsError::new("seed must be 32 bytes"))?; + Ok(Keypair::from_seed(&seed).sign(message).to_vec()) +} + +/// Verify a signature; returns a plain boolean, throws only on +/// malformed lengths. +#[wasm_bindgen] +pub fn verify_signature(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result { + let pk: [u8; PUBLIC_KEY_LEN] = public_key + .try_into() + .map_err(|_| JsError::new("public key must be 32 bytes"))?; + let sig: [u8; SIGNATURE_LEN] = signature + .try_into() + .map_err(|_| JsError::new("signature must be 64 bytes"))?; + Ok(crate::sign::verify(&pk, message, &sig)) +} + +/// SHA-384 of `data` (48 bytes). +#[wasm_bindgen] +pub fn sha384(data: &[u8]) -> Vec { + crate::hash::sha384(data).to_vec() +} From 38973f72829a320168c1b578c57373ae810db92a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 00:55:22 +0000 Subject: [PATCH 2/3] ogar-encryption: rustfmt wasm.rs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm --- crates/ogar-encryption/src/wasm.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/ogar-encryption/src/wasm.rs b/crates/ogar-encryption/src/wasm.rs index 5699870..39a84dc 100644 --- a/crates/ogar-encryption/src/wasm.rs +++ b/crates/ogar-encryption/src/wasm.rs @@ -34,8 +34,13 @@ fn params(interactive: bool) -> KdfParams { /// Seal `plaintext` under `password` client-side. With /// `interactive = true` the browser-grade Argon2id cost is used. #[wasm_bindgen] -pub fn seal_envelope(password: &[u8], plaintext: &[u8], interactive: bool) -> Result, JsError> { - envelope::seal(password, plaintext, ¶ms(interactive)).map_err(|e| JsError::new(&e.to_string())) +pub fn seal_envelope( + password: &[u8], + plaintext: &[u8], + interactive: bool, +) -> Result, JsError> { + envelope::seal(password, plaintext, ¶ms(interactive)) + .map_err(|e| JsError::new(&e.to_string())) } /// Open a sealed envelope. Throws on wrong password or tampering. @@ -75,7 +80,11 @@ pub fn sign_message(seed: &[u8], message: &[u8]) -> Result, JsError> { /// Verify a signature; returns a plain boolean, throws only on /// malformed lengths. #[wasm_bindgen] -pub fn verify_signature(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result { +pub fn verify_signature( + public_key: &[u8], + message: &[u8], + signature: &[u8], +) -> Result { let pk: [u8; PUBLIC_KEY_LEN] = public_key .try_into() .map_err(|_| JsError::new("public key must be 32 bytes"))?; From 01c2eef991542be3fff5aa77a1169b91ed0f5d6f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 01:32:32 +0000 Subject: [PATCH 3/3] Bump lance to =12.0.0 and lancedb to =0.39.0; rust-version 1.98.1 Workspace-wide sweep: lance 12 / lancedb 0.39 (lancedb 0.39 requires lance =12.0.0 and still arrow ^58 / datafusion ^54, so those are unchanged). No member crate currently uses lance or lancedb; the workspace table moves so the next consumer starts on the current pair. rust-version follows rust-toolchain.toml (1.98.1). cargo +1.98.1 check --workspace --all-targets: clean. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ba095d..89f07e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ edition = "2024" license = "MIT" repository = "https://github.com/AdaWorldAPI/OGAR" authors = ["AdaWorldAPI"] -rust-version = "1.95" +rust-version = "1.98.1" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } @@ -83,8 +83,8 @@ serde = { version = "1.0", features = ["derive"] } # crates.io upstream and NEVER from a fork — the AdaWorldAPI/lance and # /lancedb repos exist but are deliberately not depended on. # ───────────────────────────────────────────────────────────────────── -lance = "=9.0.0" -lancedb = "=0.33.0" +lance = "=12.0.0" +lancedb = "=0.39.0" datafusion = "54" arrow = "58" arrow-array = "58"