WIP: add OPAQUE password-authenticated key exchange

Add opaque-ke (v4, ristretto255) for password-based registration and
login. Extend NodeService schema with opaqueRegisterStart/Finish and
opaqueLoginStart/Finish RPCs. Add Store trait methods for OPAQUE server
setup and user records. Initial e2e integration test scaffolding.

Note: FileBackedStore does not yet implement the new Store trait
methods — server compilation is temporarily broken.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 08:25:34 +01:00
parent 9fdb37876a
commit 8d5c1b3b9b
11 changed files with 557 additions and 31 deletions

241
Cargo.lock generated
View File

@@ -164,6 +164,21 @@ version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "assert_cmd"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514"
dependencies = [
"anstyle",
"bstr",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.76" version = "0.3.76"
@@ -212,15 +227,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest 0.10.7",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.9.0" version = "0.9.0"
@@ -239,6 +245,17 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.2"
@@ -570,7 +587,9 @@ dependencies = [
"curve25519-dalek-derive", "curve25519-dalek-derive",
"digest 0.10.7", "digest 0.10.7",
"fiat-crypto", "fiat-crypto",
"rand_core 0.6.4",
"rustc_version", "rustc_version",
"serde",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -632,6 +651,23 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derive-where"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.9.0" version = "0.9.0"
@@ -653,6 +689,17 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.16.9" version = "0.16.9"
@@ -663,6 +710,7 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
"elliptic-curve", "elliptic-curve",
"rfc6979", "rfc6979",
"serdect",
"signature 2.2.0", "signature 2.2.0",
"spki", "spki",
] ]
@@ -683,6 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [ dependencies = [
"pkcs8", "pkcs8",
"serde",
"signature 2.2.0", "signature 2.2.0",
] ]
@@ -711,6 +760,7 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
"serde", "serde",
"sha2 0.10.9", "sha2 0.10.9",
"signature 2.2.0",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -738,6 +788,7 @@ dependencies = [
"pkcs8", "pkcs8",
"rand_core 0.6.4", "rand_core 0.6.4",
"sec1", "sec1",
"serdect",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -788,6 +839,12 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "ff" name = "ff"
version = "0.13.1" version = "0.13.1"
@@ -904,6 +961,7 @@ version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"serde",
"typenum", "typenum",
"version_check", "version_check",
"zeroize", "zeroize",
@@ -1198,6 +1256,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@@ -1308,6 +1372,29 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "opaque-ke"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded22991b43cd15561b62b2e1cf9ace1344a8534eebec96202d5c96a77a6616a"
dependencies = [
"curve25519-dalek 4.1.3",
"derive-where",
"digest 0.10.7",
"displaydoc",
"ecdsa",
"ed25519-dalek 2.2.0",
"elliptic-curve",
"generic-array",
"hkdf",
"hmac",
"rand 0.8.5",
"serde",
"subtle",
"voprf",
"zeroize",
]
[[package]] [[package]]
name = "openmls" name = "openmls"
version = "0.5.0" version = "0.5.0"
@@ -1509,6 +1596,15 @@ dependencies = [
"universal-hash 0.5.1", "universal-hash 0.5.1",
] ]
[[package]]
name = "portpicker"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9"
dependencies = [
"rand 0.8.5",
]
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@@ -1524,6 +1620,33 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "predicates"
version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
dependencies = [
"anstyle",
"difflib",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
[[package]]
name = "predicates-tree"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
dependencies = [
"predicates-core",
"termtree",
]
[[package]] [[package]]
name = "primeorder" name = "primeorder"
version = "0.13.6" version = "0.13.6"
@@ -1547,21 +1670,26 @@ name = "quicnprotochat-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd",
"bincode", "bincode",
"capnp", "capnp",
"capnp-rpc", "capnp-rpc",
"clap", "clap",
"dashmap", "dashmap",
"futures", "futures",
"opaque-ke",
"openmls_rust_crypto", "openmls_rust_crypto",
"portpicker",
"quicnprotochat-core", "quicnprotochat-core",
"quicnprotochat-proto", "quicnprotochat-proto",
"quinn", "quinn",
"quinn-proto", "quinn-proto",
"rand 0.8.5",
"rustls", "rustls",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
"tempfile",
"thiserror 1.0.69", "thiserror 1.0.69",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -1574,13 +1702,12 @@ name = "quicnprotochat-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"bytes",
"capnp", "capnp",
"chacha20poly1305 0.10.1", "chacha20poly1305 0.10.1",
"ed25519-dalek 2.2.0", "ed25519-dalek 2.2.0",
"futures",
"hkdf", "hkdf",
"ml-kem", "ml-kem",
"opaque-ke",
"openmls", "openmls",
"openmls_rust_crypto", "openmls_rust_crypto",
"openmls_traits", "openmls_traits",
@@ -1589,11 +1716,9 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
"snow",
"thiserror 1.0.69", "thiserror 1.0.69",
"tls_codec 0.3.0", "tls_codec 0.3.0",
"tokio", "tokio",
"tokio-util",
"x25519-dalek", "x25519-dalek",
"zeroize", "zeroize",
] ]
@@ -1617,10 +1742,12 @@ dependencies = [
"clap", "clap",
"dashmap", "dashmap",
"futures", "futures",
"opaque-ke",
"quicnprotochat-core", "quicnprotochat-core",
"quicnprotochat-proto", "quicnprotochat-proto",
"quinn", "quinn",
"quinn-proto", "quinn-proto",
"rand 0.8.5",
"rcgen", "rcgen",
"rusqlite", "rusqlite",
"rustls", "rustls",
@@ -1924,6 +2051,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.36" version = "0.23.36"
@@ -2038,6 +2178,7 @@ dependencies = [
"der", "der",
"generic-array", "generic-array",
"pkcs8", "pkcs8",
"serdect",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -2123,6 +2264,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serdect"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
dependencies = [
"base16ct",
"serde",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.9.9" version = "0.9.9"
@@ -2216,22 +2367,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "snow"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85"
dependencies = [
"aes-gcm 0.10.3",
"blake2",
"chacha20poly1305 0.10.1",
"curve25519-dalek 4.1.3",
"rand_core 0.6.4",
"rustc_version",
"sha2 0.10.9",
"subtle",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.2" version = "0.6.2"
@@ -2281,6 +2416,25 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tempfile"
version = "3.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.69" version = "1.0.69"
@@ -2615,6 +2769,34 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "voprf"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28f59c30c76e2fea54cdece6a054e2662feffa7ab19658a7887524265ee39470"
dependencies = [
"curve25519-dalek 4.1.3",
"derive-where",
"digest 0.10.7",
"displaydoc",
"elliptic-curve",
"generic-array",
"rand_core 0.6.4",
"serde",
"sha2 0.10.9",
"subtle",
"zeroize",
]
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@@ -3021,6 +3203,7 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [ dependencies = [
"serde",
"zeroize_derive", "zeroize_derive",
] ]

View File

@@ -25,6 +25,7 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
sha2 = { version = "0.10" } sha2 = { version = "0.10" }
hkdf = { version = "0.12" } hkdf = { version = "0.12" }
chacha20poly1305 = { version = "0.10" } chacha20poly1305 = { version = "0.10" }
opaque-ke = { version = "4", features = ["ristretto255"] }
zeroize = { version = "1", features = ["derive"] } zeroize = { version = "1", features = ["derive"] }
rand = { version = "0.8" } rand = { version = "0.8" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }

View File

@@ -26,6 +26,10 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
bincode = { workspace = true } bincode = { workspace = true }
# Crypto — OPAQUE PAKE
opaque-ke = { workspace = true }
rand = { workspace = true }
# Error handling # Error handling
anyhow = { workspace = true } anyhow = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
@@ -44,5 +48,7 @@ tracing-subscriber = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Integration tests use quicnprotochat-core, quicnprotochat-proto, and capnp-rpc directly.
dashmap = { workspace = true } dashmap = { workspace = true }
assert_cmd = "2"
tempfile = "3"
portpicker = "0.1"

View File

@@ -0,0 +1,164 @@
use std::{path::PathBuf, process::Command, time::Duration};
use assert_cmd::cargo::cargo_bin;
use portpicker::pick_unused_port;
use tempfile::TempDir;
use tokio::time::sleep;
use quicnprotochat_client::{
cmd_create_group, cmd_invite, cmd_join, cmd_ping, cmd_register_state, cmd_send, ClientAuth,
connect_node, fetch_wait, init_auth,
};
use quicnprotochat_core::IdentityKeypair;
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[derive(serde::Deserialize)]
struct StoredStateCompat {
identity_seed: [u8; 32],
#[allow(dead_code)]
group: Option<Vec<u8>>,
}
async fn wait_for_health(server: &str, ca_cert: &PathBuf, server_name: &str) -> anyhow::Result<()> {
let local = tokio::task::LocalSet::new();
for _ in 0..30 {
if local
.run_until(cmd_ping(server, ca_cert, server_name))
.await
.is_ok()
{
return Ok(());
}
sleep(Duration::from_millis(200)).await;
}
anyhow::bail!("server health never became ready")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn e2e_happy_path_register_invite_join_send_recv() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let base = temp.path();
let port = pick_unused_port().expect("free port");
let listen = format!("127.0.0.1:{port}");
let server = listen.clone();
let ca_cert = base.join("server-cert.der");
let tls_key = base.join("server-key.der");
let data_dir = base.join("data");
let auth_token = "devtoken";
// Spawn server binary.
let server_bin = cargo_bin("quicnprotochat-server");
let mut child = Command::new(server_bin)
.arg("--listen")
.arg(&listen)
.arg("--data-dir")
.arg(&data_dir)
.arg("--tls-cert")
.arg(&ca_cert)
.arg("--tls-key")
.arg(&tls_key)
.arg("--auth-token")
.arg(auth_token)
.spawn()
.expect("spawn server");
// Ensure we always terminate the child.
struct ChildGuard(std::process::Child);
impl Drop for ChildGuard {
fn drop(&mut self) {
let _ = self.0.kill();
}
}
let child_guard = ChildGuard(child);
let _ = child_guard;
// Wait for server to be healthy and certs to be generated.
wait_for_health(&server, &ca_cert, "localhost").await?;
// Set client auth context.
init_auth(ClientAuth::from_parts(auth_token.to_string(), None));
// LocalSet for capnp !Send operations.
let local = tokio::task::LocalSet::new();
let alice_state = base.join("alice.bin");
let bob_state = base.join("bob.bin");
local
.run_until(cmd_register_state(
&alice_state,
&server,
&ca_cert,
"localhost",
))
.await?;
local
.run_until(cmd_register_state(
&bob_state,
&server,
&ca_cert,
"localhost",
))
.await?;
local
.run_until(cmd_create_group(
&alice_state,
&server,
"test-group",
))
.await?;
// Load Bob identity key from persisted state to use as peer key.
let bob_bytes = std::fs::read(&bob_state)?;
let bob_state_compat: StoredStateCompat = bincode::deserialize(&bob_bytes)?;
let bob_identity = IdentityKeypair::from_seed(bob_state_compat.identity_seed);
let bob_pk_hex = hex_encode(&bob_identity.public_key_bytes());
local
.run_until(cmd_invite(
&alice_state,
&server,
&ca_cert,
"localhost",
&bob_pk_hex,
))
.await?;
local
.run_until(cmd_join(
&bob_state,
&server,
&ca_cert,
"localhost",
))
.await?;
// Send Alice -> Bob.
local
.run_until(cmd_send(
&alice_state,
&server,
&ca_cert,
"localhost",
&bob_pk_hex,
"hello bob",
))
.await?;
// Confirm Bob can fetch at least one payload.
local
.run_until(async {
let client = connect_node(&server, &ca_cert, "localhost").await?;
let payloads = fetch_wait(&client, &bob_identity.public_key_bytes(), 1000).await?;
anyhow::ensure!(!payloads.is_empty(), "no payloads delivered to Bob");
Ok::<(), anyhow::Error>(())
})
.await?;
Ok(())
}

View File

@@ -18,6 +18,9 @@ rand = { workspace = true }
# Crypto — post-quantum hybrid KEM (M7) # Crypto — post-quantum hybrid KEM (M7)
ml-kem = { workspace = true } ml-kem = { workspace = true }
# Crypto — OPAQUE password-authenticated key exchange
opaque-ke = { workspace = true }
# Crypto — MLS (M2) # Crypto — MLS (M2)
openmls = { workspace = true } openmls = { workspace = true }
openmls_rust_crypto = { workspace = true } openmls_rust_crypto = { workspace = true }

View File

@@ -18,6 +18,7 @@ pub mod hybrid_kem;
mod identity; mod identity;
mod keypackage; mod keypackage;
mod keystore; mod keystore;
pub mod opaque_auth;
// ── Public API ──────────────────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,22 @@
//! Shared OPAQUE (RFC 9497) cipher suite configuration.
//!
//! Both client and server import this module to ensure they use exactly
//! the same cryptographic parameters during registration and login.
use opaque_ke::CipherSuite;
/// OPAQUE cipher suite for quicnprotochat.
///
/// - **OPRF**: Ristretto255 (curve25519-based, ~128-bit security)
/// - **Key exchange**: Triple-DH (3DH) over Ristretto255 with SHA-512
/// - **KSF**: Identity (no key stretching; upgrade to Argon2 later)
pub struct OpaqueSuite;
impl CipherSuite for OpaqueSuite {
type OprfCs = opaque_ke::Ristretto255;
type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh<
opaque_ke::Ristretto255,
sha2::Sha512,
>;
type Ksf = opaque_ke::ksf::Identity;
}

View File

@@ -32,6 +32,10 @@ quinn-proto = { workspace = true }
rustls = { workspace = true } rustls = { workspace = true }
rcgen = { workspace = true } rcgen = { workspace = true }
# Crypto — OPAQUE PAKE
opaque-ke = { workspace = true }
rand = { workspace = true }
# Database # Database
rusqlite = { workspace = true } rusqlite = { workspace = true }

View File

@@ -75,7 +75,18 @@ impl SqlStore {
ON key_packages(identity_key); ON key_packages(identity_key);
CREATE INDEX IF NOT EXISTS idx_del_recipient_channel CREATE INDEX IF NOT EXISTS idx_del_recipient_channel
ON deliveries(recipient_key, channel_id);", ON deliveries(recipient_key, channel_id);
CREATE TABLE IF NOT EXISTS server_setup (
id INTEGER PRIMARY KEY CHECK (id = 1),
setup_data BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
opaque_record BLOB NOT NULL,
created_at INTEGER DEFAULT (strftime('%s','now'))
);",
) )
.map_err(|e| StorageError::Db(e.to_string()))?; .map_err(|e| StorageError::Db(e.to_string()))?;
Ok(()) Ok(())
@@ -203,6 +214,48 @@ impl Store for SqlStore {
.optional() .optional()
.map_err(|e| StorageError::Db(e.to_string())) .map_err(|e| StorageError::Db(e.to_string()))
} }
fn store_server_setup(&self, setup: Vec<u8>) -> Result<(), StorageError> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT OR REPLACE INTO server_setup (id, setup_data) VALUES (1, ?1)",
params![setup],
)
.map_err(|e| StorageError::Db(e.to_string()))?;
Ok(())
}
fn get_server_setup(&self) -> Result<Option<Vec<u8>>, StorageError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn
.prepare("SELECT setup_data FROM server_setup WHERE id = 1")
.map_err(|e| StorageError::Db(e.to_string()))?;
stmt.query_row([], |row| row.get(0))
.optional()
.map_err(|e| StorageError::Db(e.to_string()))
}
fn store_user_record(&self, username: &str, record: Vec<u8>) -> Result<(), StorageError> {
let conn = self.conn.lock().unwrap();
conn.execute(
"INSERT OR REPLACE INTO users (username, opaque_record) VALUES (?1, ?2)",
params![username, record],
)
.map_err(|e| StorageError::Db(e.to_string()))?;
Ok(())
}
fn get_user_record(&self, username: &str) -> Result<Option<Vec<u8>>, StorageError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn
.prepare("SELECT opaque_record FROM users WHERE username = ?1")
.map_err(|e| StorageError::Db(e.to_string()))?;
stmt.query_row(params![username], |row| row.get(0))
.optional()
.map_err(|e| StorageError::Db(e.to_string()))
}
} }
/// Convenience extension for `rusqlite::OptionalExtension`. /// Convenience extension for `rusqlite::OptionalExtension`.

View File

@@ -50,6 +50,18 @@ pub trait Store: Send + Sync {
) -> Result<(), StorageError>; ) -> Result<(), StorageError>;
fn fetch_hybrid_key(&self, identity_key: &[u8]) -> Result<Option<Vec<u8>>, StorageError>; fn fetch_hybrid_key(&self, identity_key: &[u8]) -> Result<Option<Vec<u8>>, StorageError>;
/// Store the OPAQUE `ServerSetup` (generated once, loaded on restart).
fn store_server_setup(&self, setup: Vec<u8>) -> Result<(), StorageError>;
/// Load the persisted `ServerSetup`, if any.
fn get_server_setup(&self) -> Result<Option<Vec<u8>>, StorageError>;
/// Store an OPAQUE user record (serialized `ServerRegistration`).
fn store_user_record(&self, username: &str, record: Vec<u8>) -> Result<(), StorageError>;
/// Retrieve an OPAQUE user record by username.
fn get_user_record(&self, username: &str) -> Result<Option<Vec<u8>>, StorageError>;
} }
// ── ChannelKey ─────────────────────────────────────────────────────────────── // ── ChannelKey ───────────────────────────────────────────────────────────────
@@ -86,9 +98,12 @@ pub struct FileBackedStore {
kp_path: PathBuf, kp_path: PathBuf,
ds_path: PathBuf, ds_path: PathBuf,
hk_path: PathBuf, hk_path: PathBuf,
setup_path: PathBuf,
users_path: PathBuf,
key_packages: Mutex<HashMap<Vec<u8>, VecDeque<Vec<u8>>>>, key_packages: Mutex<HashMap<Vec<u8>, VecDeque<Vec<u8>>>>,
deliveries: Mutex<HashMap<ChannelKey, VecDeque<Vec<u8>>>>, deliveries: Mutex<HashMap<ChannelKey, VecDeque<Vec<u8>>>>,
hybrid_keys: Mutex<HashMap<Vec<u8>, Vec<u8>>>, hybrid_keys: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
users: Mutex<HashMap<String, Vec<u8>>>,
} }
impl FileBackedStore { impl FileBackedStore {
@@ -100,18 +115,24 @@ impl FileBackedStore {
let kp_path = dir.join("keypackages.bin"); let kp_path = dir.join("keypackages.bin");
let ds_path = dir.join("deliveries.bin"); let ds_path = dir.join("deliveries.bin");
let hk_path = dir.join("hybridkeys.bin"); let hk_path = dir.join("hybridkeys.bin");
let setup_path = dir.join("server_setup.bin");
let users_path = dir.join("users.bin");
let key_packages = Mutex::new(Self::load_kp_map(&kp_path)?); let key_packages = Mutex::new(Self::load_kp_map(&kp_path)?);
let deliveries = Mutex::new(Self::load_delivery_map(&ds_path)?); let deliveries = Mutex::new(Self::load_delivery_map(&ds_path)?);
let hybrid_keys = Mutex::new(Self::load_hybrid_keys(&hk_path)?); let hybrid_keys = Mutex::new(Self::load_hybrid_keys(&hk_path)?);
let users = Mutex::new(Self::load_users(&users_path)?);
Ok(Self { Ok(Self {
kp_path, kp_path,
ds_path, ds_path,
hk_path, hk_path,
setup_path,
users_path,
key_packages, key_packages,
deliveries, deliveries,
hybrid_keys, hybrid_keys,
users,
}) })
} }
@@ -201,6 +222,29 @@ impl FileBackedStore {
} }
fs::write(path, bytes).map_err(|e| StorageError::Io(e.to_string())) fs::write(path, bytes).map_err(|e| StorageError::Io(e.to_string()))
} }
fn load_users(path: &Path) -> Result<HashMap<String, Vec<u8>>, StorageError> {
if !path.exists() {
return Ok(HashMap::new());
}
let bytes = fs::read(path).map_err(|e| StorageError::Io(e.to_string()))?;
if bytes.is_empty() {
return Ok(HashMap::new());
}
bincode::deserialize(&bytes).map_err(|_| StorageError::Serde)
}
fn flush_users(
&self,
path: &Path,
map: &HashMap<String, Vec<u8>>,
) -> Result<(), StorageError> {
let bytes = bincode::serialize(map).map_err(|_| StorageError::Serde)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?;
}
fs::write(path, bytes).map_err(|e| StorageError::Io(e.to_string()))
}
} }
impl Store for FileBackedStore { impl Store for FileBackedStore {
@@ -272,4 +316,33 @@ impl Store for FileBackedStore {
let map = self.hybrid_keys.lock().unwrap(); let map = self.hybrid_keys.lock().unwrap();
Ok(map.get(identity_key).cloned()) Ok(map.get(identity_key).cloned())
} }
fn store_server_setup(&self, setup: Vec<u8>) -> Result<(), StorageError> {
if let Some(parent) = self.setup_path.parent() {
fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?;
}
fs::write(&self.setup_path, setup).map_err(|e| StorageError::Io(e.to_string()))
}
fn get_server_setup(&self) -> Result<Option<Vec<u8>>, StorageError> {
if !self.setup_path.exists() {
return Ok(None);
}
let bytes = fs::read(&self.setup_path).map_err(|e| StorageError::Io(e.to_string()))?;
if bytes.is_empty() {
return Ok(None);
}
Ok(Some(bytes))
}
fn store_user_record(&self, username: &str, record: Vec<u8>) -> Result<(), StorageError> {
let mut map = self.users.lock().unwrap();
map.insert(username.to_string(), record);
self.flush_users(&self.users_path, &*map)
}
fn get_user_record(&self, username: &str) -> Result<Option<Vec<u8>>, StorageError> {
let map = self.users.lock().unwrap();
Ok(map.get(username).cloned())
}
} }

View File

@@ -37,6 +37,22 @@ interface NodeService {
# Fetch a peer's hybrid public key (for post-quantum envelope encryption). # Fetch a peer's hybrid public key (for post-quantum envelope encryption).
fetchHybridKey @7 (identityKey :Data) -> (hybridPublicKey :Data); fetchHybridKey @7 (identityKey :Data) -> (hybridPublicKey :Data);
# ── OPAQUE password-authenticated registration ──────────────────────────
# Start OPAQUE registration: client sends blinded password element.
opaqueRegisterStart @8 (username :Text, request :Data) -> (response :Data);
# Finish OPAQUE registration: client uploads sealed credential envelope.
opaqueRegisterFinish @9 (username :Text, upload :Data) -> (success :Bool);
# ── OPAQUE password-authenticated login ─────────────────────────────────
# Start OPAQUE login: client sends credential request.
opaqueLoginStart @10 (username :Text, request :Data) -> (response :Data);
# Finish OPAQUE login: client sends credential finalization, receives session token.
opaqueLoginFinish @11 (username :Text, finalization :Data) -> (sessionToken :Data);
} }
struct Auth { struct Auth {