diff --git a/Cargo.toml b/Cargo.toml index 23669d4..cb3f497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/ogar-adapter-python", "crates/ogar-adapter-csharp", "crates/ogar-auth", + "crates/ogar-encryption", ] [workspace.package] diff --git a/crates/ogar-auth/Cargo.toml b/crates/ogar-auth/Cargo.toml index a804364..1cb28cf 100644 --- a/crates/ogar-auth/Cargo.toml +++ b/crates/ogar-auth/Cargo.toml @@ -7,13 +7,14 @@ repository.workspace = true description = "OGAR auth arm: the reusable authentication SDK for OGAR consumers. Agnostic RFC 6238 TOTP, Argon2id PHC password hash/verify, a legacy 3DES-EDE2/PBKDF1-MD5 transition primitive (caller-supplied key table — carries no secrets), and a re-export of the ndarray `encryption` forward suite (XChaCha20-Poly1305 / SHA-384 / Ed25519 / zero-knowledge envelope). One surface every consumer pulls, so the auth primitives never diverge. Endgame seam: OGIT[auth] IAM federation (OIDC / Zitadel)." [dependencies] -# Forward suite — REUSED from the ndarray fork, never re-implemented here. -# Argon2id KDF, XChaCha20-Poly1305 AEAD, Ed25519 signatures, SHA-384, and the -# wasm-capable zero-knowledge envelope all live in `encryption`; ogar-auth -# re-exports them (see lib.rs) so a consumer imports ONE crate. Git dep, -# matching the ogar-class-view → lance-graph-contract precedent (ndarray's -# default branch is `master`). -encryption = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master" } +# Forward suite — REUSED via OGAR's own generic encryption surface, never +# re-implemented here. Argon2id KDF, XChaCha20-Poly1305 AEAD, Ed25519 +# signatures, SHA-384, and the wasm-capable zero-knowledge envelope all live +# in `encryption` (the ndarray fork); `ogar-encryption` is the single +# classid-agnostic re-export every Ada consumer pulls, and ogar-auth builds +# its auth-specific flows (password/totp/legacy) on top of it. Path dep, +# in-workspace sibling. +ogar-encryption = { path = "../ogar-encryption" } # Argon2id PHC hash+verify (the login-credential path — distinct from the # envelope KDF). Default features carry `password-hash` + `rand` (OsRng salt). diff --git a/crates/ogar-auth/src/lib.rs b/crates/ogar-auth/src/lib.rs index 048662d..d632f2f 100644 --- a/crates/ogar-auth/src/lib.rs +++ b/crates/ogar-auth/src/lib.rs @@ -19,12 +19,14 @@ //! | [`envelope`] (re-export) | seal / open | wasm-capable zero-knowledge envelope | //! //! The forward suite (`kdf` / `aead` / `hash` / `sign` / `envelope`) is -//! **re-exported from the ndarray `encryption` crate, never re-implemented**. -//! That crate is the audited, wasm-capable home of XChaCha20-Poly1305 + -//! Argon2id + Ed25519 + SHA-384; duplicating its byte-exact envelope layout -//! here would be exactly the dilution ogar-auth prevents. The three modules -//! this crate *adds* (`password`, `totp`, `legacy`) are the auth-specific -//! primitives the encryption crate deliberately does not carry. +//! **re-exported via [`ogar-encryption`](https://github.com/AdaWorldAPI/OGAR), +//! never re-implemented**. `ogar-encryption` is itself a thin, classid-agnostic +//! re-export of the ndarray `encryption` crate — the audited, wasm-capable +//! home of XChaCha20-Poly1305 + Argon2id + Ed25519 + SHA-384; duplicating its +//! byte-exact envelope layout here would be exactly the dilution ogar-auth +//! prevents. The three modules this crate *adds* (`password`, `totp`, +//! `legacy`) are the auth-specific primitives neither `ogar-encryption` nor +//! the encryption crate it wraps deliberately carries. //! //! ## Agnostic by construction — no consumer secrets //! @@ -50,10 +52,10 @@ pub mod legacy; pub mod password; pub mod totp; -// ── Forward suite: re-exported from the ndarray `encryption` crate ────────── +// ── Forward suite: re-exported via `ogar-encryption` ──────────────────────── // Reused wholesale, never re-implemented (see crate docs). A consumer that // pulls `ogar-auth` gets the entire forward crypto surface under one import. -pub use encryption::{aead, envelope, hash, kdf, sign}; +pub use ogar_encryption::{aead, envelope, hash, kdf, sign}; /// Unified error for the auth-specific primitives this crate adds /// (`password`, `totp`, `legacy`). Field-free on the secret-bearing paths so diff --git a/crates/ogar-encryption/Cargo.toml b/crates/ogar-encryption/Cargo.toml new file mode 100644 index 0000000..a4128e5 --- /dev/null +++ b/crates/ogar-encryption/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ogar-encryption" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "OGAR's single, generic, classid-agnostic encryption surface: a thin re-export of the ndarray `encryption` crate (Argon2id KDF, XChaCha20-Poly1305 AEAD, Ed25519 signatures, SHA-384 hash, the wasm-capable zero-knowledge seal/open envelope). Every Ada consumer that needs raw forward crypto pulls THIS crate rather than depping the ndarray fork directly or hand-rolling argon2/chacha/ed25519. Carries no secrets and no consumer specifics; ogar-auth builds its auth-specific flows (password/totp/legacy) on top of this." + +[dependencies] +# The forward crypto suite — REUSED from the ndarray fork, never re-implemented +# here. Argon2id KDF, XChaCha20-Poly1305 AEAD, Ed25519 signatures, SHA-384, and +# the wasm-capable zero-knowledge envelope all live in `encryption`; this crate +# re-exports them verbatim (see lib.rs) so every consumer imports ONE crate. +# Git dep, matching the ogar-auth precedent (ndarray's default branch is +# `master`). +encryption = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master" } + +[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"] diff --git a/crates/ogar-encryption/src/lib.rs b/crates/ogar-encryption/src/lib.rs new file mode 100644 index 0000000..41a4c9f --- /dev/null +++ b/crates/ogar-encryption/src/lib.rs @@ -0,0 +1,74 @@ +//! # ogar-encryption — OGAR's generic encryption surface +//! +//! One small, classid-agnostic crate re-exporting the Ada stack's forward +//! crypto suite, so every OGAR consumer that needs raw encryption imports +//! **exactly one crate** instead of depping the ndarray fork directly or +//! (worse) hand-rolling its own Argon2/XChaCha20/Ed25519 wrapper. +//! +//! ## What lives here +//! +//! Nothing is implemented in this crate. It is a thin, documented re-export +//! of [`encryption`] (the ndarray fork's audited, wasm-capable crypto +//! module): +//! +//! | Module / item | Primitive | Role | +//! |---|---|---| +//! | [`kdf`] | Argon2id | password/secret → raw key derivation | +//! | [`aead`] | XChaCha20-Poly1305 | authenticated encryption | +//! | [`hash`] | SHA-384 | merkle / fingerprint hashing | +//! | [`sign`] | Ed25519 | licence / audit signatures | +//! | [`envelope`] | seal / open | wasm-capable zero-knowledge envelope | +//! | [`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 | +//! +//! ## Generic, classid-agnostic, no secrets — by construction +//! +//! This crate carries **no consumer specifics**: no classid, no tenant, no +//! key material, no wire DTO. It is pure re-export surface — every symbol +//! here is exactly what [`encryption`] exports, unmodified. That is the +//! point: the forward suite must never diverge into per-consumer copies, and +//! a crate that re-exports and adds nothing cannot dilute it. +//! +//! ## Who builds on this +//! +//! - [`ogar-auth`](https://github.com/AdaWorldAPI/OGAR) depends on +//! `ogar-encryption` for the forward suite and adds the auth-specific +//! primitives the encryption crate deliberately does not carry (Argon2id +//! PHC password hash/verify, RFC 6238 TOTP, the legacy 3DES-EDE2/PBKDF1-MD5 +//! transition cipher). +//! - Every other Ada consumer (medcare-rs, woa-rs, smb-office-rs, and +//! siblings) that needs raw forward crypto — sealing a secret client-side, +//! verifying a licence signature, hashing for a merkle chain — pulls +//! `ogar-encryption` directly rather than the ndarray fork or a hand-rolled +//! equivalent. +//! +//! ## wasm build +//! +//! ```text +//! cargo check -p ogar-encryption --target wasm32-unknown-unknown +//! cargo build -p ogar-encryption --target wasm32-unknown-unknown \ +//! --features wasm --release +//! ``` + +#![forbid(unsafe_code)] + +// ── The forward suite: re-exported wholesale from the ndarray `encryption` +// crate. Reused, never re-implemented (see crate docs). A consumer that pulls +// `ogar-encryption` gets the entire forward crypto surface under one import. +pub use encryption::{aead, envelope, hash, kdf, sign}; + +// ── Root-level convenience aliases, mirrored from `encryption`'s own root +// re-exports (`envelope::{seal, open}` plus the envelope's error/parameter +// types), so callers that used the upstream crate's short paths keep them. +pub use encryption::{open, seal, EnvelopeError, KdfParams}; + +// ── The platform-CSPRNG-unavailable error, mirrored from `encryption`'s +// crate root. +pub use encryption::RngError; + +/// wasm-bindgen bindings for browser consumers (forwarded from +/// [`encryption`]'s `wasm-bindings` feature via this crate's `wasm` feature). +#[cfg(feature = "wasm")] +pub use encryption::wasm; diff --git a/docs/OGAR-CRYPTO-CONSUMER-MIGRATION.md b/docs/OGAR-CRYPTO-CONSUMER-MIGRATION.md new file mode 100644 index 0000000..3fc4ff1 --- /dev/null +++ b/docs/OGAR-CRYPTO-CONSUMER-MIGRATION.md @@ -0,0 +1,120 @@ +# OGAR crypto — consumer migration plan (ogar-encryption / ogar-auth / ogar-rbac) + +> **Goal (operator):** make `ogar-auth` / `ogar-encryption` / `ogar-rbac` +> globally reusable for all consumers, backed by `AdaWorldAPI/crypto` (= the +> ndarray `encryption` crate) wired through the `ndarray::simd::*` hardware- +> acceleration layer (server AVX-512 / browser wasm128). Argon2id + XChaCha20- +> Poly1305 etc. run on that one accelerated spine, native and wasm. +> +> **Status:** the generic surface exists — `ogar-encryption` (this repo, thin +> re-export of the `encryption` crate), `ogar-auth` (password/totp/legacy on top), +> `lance-graph-rbac` (the `0x0B` auth membrane / `ClassRbac`). The +> `ndarray::simd` ChaCha20 ARX keystream is **complete and trusted**: scalar +> reference + **AVX-512 (server)** + **wasm128 (browser)** backends, all +> byte-parity-proven against both the RFC 8439 KAT and the vetted RustCrypto +> `chacha20` implementation across the parameter space (`chacha20_keystream`, +> ndarray branch `claude/medcare-ruff-codebook-handover-5ulx0i`). + +## The one crypto+SIMD spine + +``` +ndarray::simd::chacha20_block (ARX keystream, AVX-512 / wasm128 / NEON / scalar) + ▲ hot path +ndarray crates/encryption (Argon2id · XChaCha20-Poly1305 · Ed25519 · SHA-384 · envelope · wasm) + ▲ re-export (never re-implement) +ogar-encryption (generic, classid-agnostic raw-crypto surface) ← ALL consumers pull this + ▲ +ogar-auth (+ password PHC · RFC-6238 TOTP · legacy 3DES bridge) +lance-graph-rbac (authorize() · ClassRbac · 0x0B membrane) ← the "ogar-rbac" role +``` + +Consumers pull `ogar-encryption` for raw crypto, `ogar-auth` for login/2FA, +`lance-graph-rbac` (directly, or via `lance-graph-callcenter`'s re-export where a +BBB allowlist forbids the direct dep) for authorization. **Never hand-roll +argon2/chacha/ed25519; never copy the codebook.** + +## Migration tiers (from the 2026-07-11 cross-repo reusability audit) + +### Tier 1 — byte-compatible, drop-in (same `Argon2::default()` → identical PHC) +| Consumer | Site | → pull | +|---|---|---| +| MedCare-rs | `medcare-core::crypto::{hash,verify}_password` | `ogar-auth::password` | +| MedCare-rs | `medcare-core::totp` (near-verbatim duplicate) | `ogar-auth::totp` | +| woa-rs | `src/auth/mod.rs::{hash_password,verify_argon2id}` | `ogar-auth::password` | +| openproject-nexgen-rs | `op-auth::api_key` Argon2 arm | `ogar-auth::password` | + +#### MedCare-rs Tier-1 — ✅ SHIPPED (2026-07-11, medcare-rs `6d2c372`) + +`medcare-core::crypto::{hash,verify}_password` and the whole `medcare-core::totp` +module now **delegate to `ogar-auth`** (body-only, public signatures preserved). +Empirically byte-compatible: medcare-core's own crypto+totp tests — including the +RFC 6238 Appendix-B vectors and the Argon2 hash/verify round-trip — pass **through** +the delegated ogar-auth path (56/56). medcare-core / medcare-db / medcare-server all +compile. The dep-wiring decision below was resolved as **option (b)**: a root +`[patch."…/ndarray"] encryption = { path = "vendor/ndarray/crates/encryption" }` +folds `ogar-encryption`'s git `encryption` onto the locally-vendored fork copy — one +source, no git fetch, no OGAR change. (The `encryption` crate has no ndarray dep, so +the patch touches only that leaf.) The recipe + rationale below are retained as the +template for the woa-rs / openproject Tier-1 migrations. + +Byte-compatibility was **confirmed by source read** (2026-07-11) before wiring: `ogar-auth::password` +is `Argon2::default()` (identical PHC to `medcare-core::crypto`), and `ogar-auth::totp` +is a near-verbatim copy of `medcare-core::totp` — same `STEP_SECONDS=30`/`DIGITS=6`/ +`SKEW_STEPS=1`, same RFC 4648 base32, same RFC 4226 HOTP, same RFC 6238 Appendix-B +vectors, same `otpauth://` URI. So the migration is a **body-only delegation** that +preserves every `medcare-core` public signature (zero caller changes in medcare-db / +medcare-server): + +1. `medcare-core/Cargo.toml`: add `ogar-auth = { path = "../../vendor/OGAR/crates/ogar-auth" }`. +2. `medcare-core/src/crypto.rs`: replace the `hash_password`/`verify_password` **bodies** + with `ogar_auth::password::*`, mapping `AuthError → DomainError::Crypto`. Keep AES-GCM + + SHA-256 local (Tier 2/3). +3. `medcare-core/src/totp.rs`: replace `base32_{encode,decode}` / `hotp` / `code_at` / + `verify_code` / `provisioning_uri` / `generate_secret_base32` **bodies** with + `ogar_auth::totp::*`, mapping errors; keep the existing medcare tests (they now + exercise the delegated path and must stay green). One signature mismatch to bridge: + ogar-auth `generate_secret_base32()` is `AuthResult` (getrandom) vs medcare's + infallible `-> String` — either propagate the Result (touches 1-2 callers) or map a + CSPRNG failure explicitly; do NOT paper over it. + +**⚠ Dep-wiring decision the operator must make first** (why this is staged, not shipped): +`ogar-auth` → `ogar-encryption` → `encryption` is a **git** dep (`ndarray` `branch="master"`), +so depping `ogar-auth` into `medcare-core` git-fetches an `encryption`/`ndarray` tree into a +workspace that otherwise **vendors ndarray locally** (`vendor/ndarray -> ../../ndarray`). That +is the established `ogar-encryption` wiring, not a new inconsistency — but it means a second, +git-sourced ndarray-encryption alongside the locally-vendored one, plus a heavier first build. +Resolve before wiring: either (a) accept the git `encryption` pull as-is, or (b) repoint +`ogar-encryption`'s `encryption` dep at the local path so all consumers share one ndarray +source (the fork-policy-consistent option). Only `password`+`totp` are used here, but depping +`ogar-auth` pulls the whole forward suite (incl. the git `encryption`) regardless. + +### Tier 2 — data-compat (different algorithm/format → coordinated rekey, not drop-in) +| Consumer | Site | Note | +|---|---|---| +| MedCare-rs | `medcare-core::crypto` AES-256-GCM (DMS blobs) | XChaCha20-Poly1305 ≠ AES-GCM; decrypt-old/encrypt-new pass over `pma_dokument` + key rotation | +| MedCare-rs | `legacy_crypt.rs` (3DES-EDE2/PBKDF1-MD5) | `ogar-auth::legacy` exists but BOTH sides UNVERIFIED vs prod ciphertext — pair with the byte-parity vector work, don't swap blind | +| woa-rs | RFC-005 Fernet→chacha vault (unimplemented) | build straight onto `ogar-encryption::{aead,kdf}`, amend RFC-005 first | +| Sharepoint-rs | `smb-policy-encryption` (`aes_gcm_kv` stub, no Cargo.toml) | greenfield — scaffold on `ogar-encryption` (XChaCha), drop the AES-GCM name | + +### Tier 3 — keep-local (justified) +- Content-integrity **SHA-256** (MedCare hardware/licence/dms; woa GoBD audit-chain; op-attachments) — hashing records, not secrets; the envelope's SHA-**384** is a different width for its own use. +- woa-rs legacy salted-SHA256 portal format — pinned to Python `hashlib.sha256(salt+pw)` for writer-parity. +- **JWT session mint** (MedCare-rs, openproject) — a genuine *surface gap*: no generic JWT/session type in ogar-auth yet (candidate `ogar-auth::session`). +- spider PKCE SHA-256 — RFC 7636, non-secret. + +## RBAC ("ogar-rbac") status +- **MedCare-rs** `medcare-rbac` and **woa-rs** `unified_bridge.rs` already route `Policy` through `lance_graph_rbac` — the reference pattern (woa via callcenter's re-export to honor its BBB allowlist). +- **openproject-nexgen-rs** `op-auth::permissions.rs` hand-rolls a bespoke permission engine → should map OpenProject permission *names* over `lance_graph_rbac::policy::Policy`, not a parallel engine. +- **Sharepoint-rs** `smb-policy-tenant-rbac` — declared workspace member, no source; scaffold on `lance-graph-rbac` from the first commit. + +## Blockers to confirm before wiring +- **woa-rs BBB-Barriere** (`.claude/CLAUDE.md` §3 Iron Rule 1): allowlist = `lance-graph-{contract,ontology,callcenter}`; `lance-graph-rbac` is VERBOTEN direct. `ogar-auth`/`ogar-encryption` are a *different namespace*, named in neither list → **needs an explicit allowlist ruling** before woa-rs deps them. (MedCare-rs & openproject have no such barrier — same-pattern add.) +- **Sharepoint-rs** won't `cargo build` today (3 members missing `Cargo.toml`, 1 missing directory) — nothing to migrate yet; fix by scaffolding onto the generic surface. + +## Sequencing +1. ~~Land the AVX-512 / wasm128 backends of `chacha20_block` (parity vs the scalar KAT).~~ **DONE.** Both backends shipped, byte-parity-proven vs the scalar KAT *and* vs the vetted RustCrypto `chacha20` across the parameter space. The keystream primitive is trusted. +2. **The `encryption` AEAD is NOT re-wired to the primitive — by security ruling, not omission.** XChaCha20-Poly1305 = HChaCha20 subkey + Poly1305 one-time-key + MAC/AAD/length framing, not just a keystream. Swapping only the keystream means hand-composing that authenticated construction — the "roll your own AEAD" footgun the stack forbids ("never roll your own crypto — wrap vetted RustCrypto"). The RustCrypto `XChaCha20Poly1305` stays. The accelerated primitive is for **raw-stream** use sites whose caller already owns a vetted MAC/framing (e.g. a future woa-rs RFC-005 vault built on `ogar-encryption::{aead,kdf}`), never for reimplementing an AEAD. See the doc-comment in `encryption/src/aead.rs`. **OPEN DECISION for the operator:** whether to add `ndarray` as an *optional* dep of the lean `encryption` crate to expose a raw-stream `apply_keystream` at all — it pulls a heavy dep and the crate's wasm build currently has an unrelated `getrandom` `wasm_js` config gap. Deferred pending that call. +3. Tier-1 drop-in migrations (password/totp → `ogar-auth`). **MedCare-rs DONE** (`6d2c372`, delegated + 56/56 green + all crates compile; dep-wiring resolved via the local-`encryption` patch). openproject-nexgen-rs next (no barrier, same pattern). +4. Confirm the woa-rs allowlist ruling (the `ogar-*` namespace is in neither BBB list); then woa-rs Tier-1. +5. Tier-2 data-compat migrations behind their rekey plans. +6. Fill the JWT-session gap (`ogar-auth::session`) if the consumers want to converge it.