Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down Expand Up @@ -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"
Expand Down
15 changes: 10 additions & 5 deletions crates/ogar-encryption/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
2 changes: 1 addition & 1 deletion crates/ogar-encryption/src/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
///
Expand Down
11 changes: 4 additions & 7 deletions crates/ogar-encryption/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand Down Expand Up @@ -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;
101 changes: 101 additions & 0 deletions crates/ogar-encryption/src/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! 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<Vec<u8>, JsError> {
envelope::seal(password, plaintext, &params(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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<bool, JsError> {
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<u8> {
crate::hash::sha384(data).to_vec()
}
Loading