diff --git a/Cargo.lock b/Cargo.lock
index 85eecc3..6b7ac8a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -20,7 +20,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
- "cpufeatures",
+ "cpufeatures 0.2.17",
]
[[package]]
@@ -37,6 +37,27 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "anstream"
version = "1.0.0"
@@ -73,7 +94,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -84,7 +105,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -101,10 +122,16 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"password-hash",
]
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
[[package]]
name = "askama"
version = "0.12.1"
@@ -146,6 +173,35 @@ dependencies = [
"nom",
]
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "async-compat"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c97d7ff3c25d6c10d64170c12acaf5d4245e76dece3779c1d92b153a64f11df"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "once_cell",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
[[package]]
name = "autocfg"
version = "1.5.1"
@@ -164,6 +220,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "base64ct"
version = "1.8.3"
@@ -188,6 +250,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
[[package]]
name = "blake2"
version = "0.10.6"
@@ -197,6 +265,19 @@ dependencies = [
"digest",
]
+[[package]]
+name = "blake3"
+version = "1.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
+dependencies = [
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.1",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -250,6 +331,16 @@ dependencies = [
"thiserror 1.0.69",
]
+[[package]]
+name = "cc"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
[[package]]
name = "cfg-if"
version = "1.0.5"
@@ -264,7 +355,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
- "cpufeatures",
+ "cpufeatures 0.2.17",
]
[[package]]
@@ -280,6 +371,33 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
[[package]]
name = "cipher"
version = "0.4.4"
@@ -343,6 +461,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -352,6 +476,21 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
[[package]]
name = "crypto-bigint"
version = "0.5.5"
@@ -391,7 +530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
@@ -418,6 +557,24 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+[[package]]
+name = "deadpool"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b"
+dependencies = [
+ "deadpool-runtime",
+ "lazy_static",
+ "num_cpus",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool-runtime"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
+
[[package]]
name = "der"
version = "0.7.10"
@@ -425,9 +582,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
+ "pem-rfc7468",
"zeroize",
]
+[[package]]
+name = "deranged"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
+
[[package]]
name = "derive-where"
version = "1.7.0"
@@ -446,6 +610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
"subtle",
]
@@ -461,6 +626,20 @@ dependencies = [
"syn 3.0.9",
]
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
[[package]]
name = "ed25519"
version = "2.2.3"
@@ -499,12 +678,48 @@ dependencies = [
"generic-array",
"group",
"hkdf",
+ "pem-rfc7468",
+ "pkcs8",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
+[[package]]
+name = "equivalent"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
[[package]]
name = "ff"
version = "0.13.1"
@@ -521,6 +736,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -539,12 +766,71 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "futures"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
[[package]]
name = "futures-task"
version = "0.3.34"
@@ -557,8 +843,13 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
+ "futures-channel",
"futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
"futures-task",
+ "memchr",
"pin-project-lite",
"slab",
]
@@ -569,6 +860,7 @@ version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
+ "serde",
"typenum",
"version_check",
"zeroize",
@@ -636,12 +928,72 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "h2"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
[[package]]
name = "hkdf"
version = "0.12.4"
@@ -681,6 +1033,112 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
+dependencies = [
+ "base64 0.23.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -785,6 +1243,16 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
[[package]]
name = "inout"
version = "0.1.4"
@@ -794,6 +1262,12 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "ipnet"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -823,11 +1297,14 @@ version = "0.1.0"
dependencies = [
"argon2",
"chacha20poly1305",
+ "data-encoding",
"ed25519-dalek",
"hkdf",
"hpke",
+ "kv-model",
"kv-proto",
"rand_core",
+ "serde_json",
"sha2",
"thiserror 2.0.21",
"unicode-normalization",
@@ -839,11 +1316,18 @@ dependencies = [
name = "kv-ffi"
version = "0.1.0"
dependencies = [
+ "hkdf",
"kv-crypto",
"kv-model",
"kv-proto",
+ "kv-store",
+ "kv-sync",
+ "serde",
"serde_json",
+ "sha2",
+ "tempfile",
"thiserror 2.0.21",
+ "tokio",
"uniffi",
"uuid",
"zeroize",
@@ -871,7 +1355,7 @@ dependencies = [
name = "kv-proto"
version = "0.1.0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"opaque-ke",
"rand_core",
"serde",
@@ -885,6 +1369,9 @@ name = "kv-store"
version = "0.1.0"
dependencies = [
"kv-proto",
+ "rusqlite",
+ "serde_json",
+ "tempfile",
"thiserror 2.0.21",
"uuid",
]
@@ -893,17 +1380,65 @@ dependencies = [
name = "kv-sync"
version = "0.1.0"
dependencies = [
+ "base64 0.22.1",
+ "blake3",
+ "ciborium",
+ "kv-crypto",
+ "kv-model",
"kv-proto",
+ "kv-store",
+ "kv-sync",
+ "opaque-ke",
+ "p256",
+ "rand_core",
+ "rcgen",
+ "reqwest",
+ "rustls",
+ "rustls-webpki",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tempfile",
"thiserror 2.0.21",
+ "time",
+ "tokio",
+ "tokio-rustls",
+ "url",
"uuid",
+ "webpki-roots 0.26.11",
+ "wiremock",
+ "zeroize",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
+
[[package]]
name = "libc"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
[[package]]
name = "litemap"
version = "0.8.3"
@@ -944,6 +1479,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+[[package]]
+name = "mio"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "nom"
version = "7.1.3"
@@ -954,6 +1500,22 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -988,6 +1550,7 @@ dependencies = [
"hkdf",
"hmac",
"rand",
+ "serde",
"subtle",
"voprf",
"zeroize",
@@ -999,8 +1562,10 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
+ "ecdsa",
"elliptic-curve",
"primeorder",
+ "sha2",
]
[[package]]
@@ -1020,6 +1585,25 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64 0.22.1",
+ "serde_core",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1042,6 +1626,12 @@ dependencies = [
"spki",
]
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
[[package]]
name = "plain"
version = "0.2.3"
@@ -1054,7 +1644,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
- "cpufeatures",
+ "cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -1066,7 +1656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
@@ -1080,6 +1670,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "powerfmt"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1167,6 +1763,123 @@ dependencies = [
"getrandom 0.2.17",
]
+[[package]]
+name = "rcgen"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+dependencies = [
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "yasna",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -1176,12 +1889,66 @@ dependencies = [
"semver",
]
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
[[package]]
name = "scroll"
version = "0.12.0"
@@ -1211,6 +1978,7 @@ dependencies = [
"base16ct",
"der",
"generic-array",
+ "pkcs8",
"subtle",
"zeroize",
]
@@ -1268,6 +2036,18 @@ dependencies = [
"zmij",
]
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
[[package]]
name = "sha1"
version = "0.10.7"
@@ -1275,7 +2055,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
@@ -1286,16 +2066,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
- "cpufeatures",
+ "cpufeatures 0.2.17",
"digest",
]
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
+ "digest",
"rand_core",
]
@@ -1323,6 +2110,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "spki"
version = "0.7.3"
@@ -1379,6 +2176,15 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
[[package]]
name = "synstructure"
version = "0.14.0"
@@ -1390,6 +2196,19 @@ dependencies = [
"syn 3.0.9",
]
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "textwrap"
version = "0.16.4"
@@ -1440,6 +2259,36 @@ dependencies = [
"syn 3.0.9",
]
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
[[package]]
name = "tinystr"
version = "0.8.4"
@@ -1456,6 +2305,56 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
+[[package]]
+name = "tokio"
+version = "1.53.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.9",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "libc",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "toml"
version = "0.5.11"
@@ -1465,6 +2364,76 @@ dependencies = [
"serde",
]
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
[[package]]
name = "typenum"
version = "1.20.1"
@@ -1553,6 +2522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f"
dependencies = [
"anyhow",
+ "async-compat",
"bytes",
"log",
"once_cell",
@@ -1626,6 +2596,12 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.8"
@@ -1662,6 +2638,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -1681,11 +2663,21 @@ dependencies = [
"elliptic-curve",
"generic-array",
"rand_core",
+ "serde",
"sha2",
"subtle",
"zeroize",
]
+[[package]]
+name = "want"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b"
+dependencies = [
+ "try-lock",
+]
+
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -1705,6 +2697,17 @@ dependencies = [
"wasm-bindgen-shared",
]
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
+dependencies = [
+ "js-sys",
+ "tokio",
+ "wasm-bindgen",
+]
+
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.129"
@@ -1737,6 +2740,34 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "web-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.9",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "weedle2"
version = "5.0.0"
@@ -1752,6 +2783,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -1761,6 +2801,93 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "wiremock"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
+dependencies = [
+ "assert-json-diff",
+ "base64 0.22.1",
+ "deadpool",
+ "futures",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "tokio",
+ "url",
+]
+
[[package]]
name = "writeable"
version = "0.6.4"
@@ -1777,6 +2904,15 @@ dependencies = [
"rand_core",
]
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
[[package]]
name = "yoke"
version = "0.8.3"
diff --git a/Cargo.toml b/Cargo.toml
index 8465b8e..2912ce6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,21 @@ data-encoding = "2"
url = "2"
psl = "2"
uniffi = "0.28"
+rusqlite = { version = "0.32", features = ["bundled"] }
+opaque-ke = { version = "3", features = ["std"] }
+reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-manual-roots-no-provider", "http2"] }
+rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
+webpki-roots = "0.26"
+rustls-webpki = { version = "0.103", default-features = false, features = ["alloc", "ring"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync", "net", "io-util"] }
+ciborium = "0.2"
+blake3 = "1"
+p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
+time = { version = "0.3", features = ["formatting"] }
+wiremock = "0.6"
+rcgen = "0.13"
+tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
+tempfile = "3"
[profile.release]
lto = true
diff --git a/README.md b/README.md
index ae65363..efffcd6 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@ Android and desktop apps call it through UniFFI. The server uses only `kv-proto`
| `kv-proto` | ✅ M0 | Wire types (Envelope, KDF params, API bodies). No secrets. |
| `kv-crypto` | ✅ M0 | 2SKD (Argon2id + Secret Key), XChaCha20-Poly1305 envelopes, HPKE vault-key sealing, keyset, Ed25519, Secret Key format |
| `kv-model` | ✅ M0 | Items, URL matching (PSL), TOTP (RFC 6238), password generator |
-| `kv-ffi` | ✅ M0 (in-memory) | The exported API (`VaultCore`, `totp_code`, `match_url`, `generate_password`) |
-| `kv-store` | 🔜 M1 | SQLite replica (ciphertext only) |
-| `kv-sync` | 🔜 M1 | OPAQUE login, RFC 9421 signing, pull/push/merge, signed manifest |
+| `kv-ffi` | ✅ M1 | The exported API (`VaultCore` with SQLite storage, local unlock key, sync; `totp_code`, `match_url`, `generate_password`) |
+| `kv-store` | ✅ M1 | SQLite replica (ciphertext only), base copies for merges, 10-version history |
+| `kv-sync` | ✅ M1 | TLS 1.3 + SPKI pins, OPAQUE login, RFC 9421 signing, pull/push/merge, signed manifest |
Spec: `../kestrove-spec/docs/02-cryptography.md` and `03-sync-protocol.md`.
@@ -30,6 +30,13 @@ cargo test --workspace
The desktop (Tauri) app depends on `kv-ffi` directly as a Rust crate.
+## Test vectors and the live-server test
+
+```bash
+cargo run -p kv-crypto --example gen-vectors # writes crates/kv-crypto/tests/vectors and ../kestrove-spec/vectors
+cargo test -p kv-ffi --test live_server -- --ignored # against a fresh local kestrove-server; see the test's docs
+```
+
## Rules
* No `unsafe` outside generated FFI glue.
* Secrets live in `Zeroizing<_>`. Never log, `Debug`-print or `format!` a secret.
diff --git a/crates/kv-crypto/Cargo.toml b/crates/kv-crypto/Cargo.toml
index 12b0238..6d3ff56 100644
--- a/crates/kv-crypto/Cargo.toml
+++ b/crates/kv-crypto/Cargo.toml
@@ -19,3 +19,12 @@ chacha20poly1305.workspace = true
hpke.workspace = true
ed25519-dalek.workspace = true
unicode-normalization.workspace = true
+
+[dev-dependencies]
+kv-model.workspace = true
+serde_json.workspace = true
+data-encoding.workspace = true
+
+[[example]]
+name = "gen-vectors"
+path = "examples/gen-vectors.rs"
diff --git a/crates/kv-crypto/examples/gen-vectors.rs b/crates/kv-crypto/examples/gen-vectors.rs
new file mode 100644
index 0000000..7ad78ce
--- /dev/null
+++ b/crates/kv-crypto/examples/gen-vectors.rs
@@ -0,0 +1,32 @@
+//! Writes the cross-implementation test vectors.
+//!
+//! ```sh
+//! cargo run -p kv-crypto --example gen-vectors # in-repo copy + ../kestrove-spec/vectors if present
+//! cargo run -p kv-crypto --example gen-vectors --
# explicit output directory
+//! ```
+
+use std::path::PathBuf;
+
+#[path = "../vectors/gen.rs"]
+mod gen;
+
+fn main() {
+ let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let mut dirs: Vec = std::env::args().skip(1).map(PathBuf::from).collect();
+ if dirs.is_empty() {
+ dirs.push(manifest.join("tests/vectors"));
+ let spec = manifest.join("../../../kestrove-spec/vectors");
+ if spec.is_dir() {
+ dirs.push(spec);
+ }
+ }
+ let docs = gen::generate();
+ for dir in &dirs {
+ std::fs::create_dir_all(dir).expect("create output dir");
+ for (name, doc) in &docs {
+ let path = dir.join(format!("{name}.json"));
+ std::fs::write(&path, gen::render(doc)).expect("write vector file");
+ println!("wrote {}", path.display());
+ }
+ }
+}
diff --git a/crates/kv-crypto/src/aead.rs b/crates/kv-crypto/src/aead.rs
index a67817d..013358c 100644
--- a/crates/kv-crypto/src/aead.rs
+++ b/crates/kv-crypto/src/aead.rs
@@ -30,8 +30,13 @@ impl SymKey {
}
pub fn seal(&self, plaintext: &[u8], aad: &[u8]) -> Result {
+ self.seal_with_nonce(random_bytes(), plaintext, aad)
+ }
+
+ /// Deterministic variant for test vectors. Never reuse a nonce with the same key.
+ #[doc(hidden)]
+ pub fn seal_with_nonce(&self, nonce: [u8; 24], plaintext: &[u8], aad: &[u8]) -> Result {
let cipher = XChaCha20Poly1305::new(Key::from_slice(&self.bytes[..]));
- let nonce: [u8; 24] = random_bytes();
let ct = cipher
.encrypt(XNonce::from_slice(&nonce), Payload { msg: plaintext, aad })
.map_err(|_| CryptoError::Encrypt)?;
diff --git a/crates/kv-crypto/src/kdf.rs b/crates/kv-crypto/src/kdf.rs
index a50d673..4d1d2c9 100644
--- a/crates/kv-crypto/src/kdf.rs
+++ b/crates/kv-crypto/src/kdf.rs
@@ -12,6 +12,8 @@ use crate::{random_bytes, secret_key::SecretKey, CryptoError, Result};
/// Keys derived from master password + Secret Key.
pub struct DerivedKeys {
+ /// Root key both outputs are expanded from. Exposed for test vectors only.
+ pub root: Zeroizing<[u8; 32]>,
/// Account Unlock Key: wraps the account keyset.
pub auk: Zeroizing<[u8; 32]>,
/// Fed to OPAQUE as the "password". Independent of `auk`.
@@ -90,7 +92,7 @@ pub(crate) fn derive_keys_unchecked(
hk.expand(b"kestrove/v1/auk", auk.as_mut()).map_err(|_| CryptoError::Kdf)?;
hk.expand(b"kestrove/v1/opaque-pwd", auth_input.as_mut()).map_err(|_| CryptoError::Kdf)?;
- Ok(DerivedKeys { auk, auth_input })
+ Ok(DerivedKeys { root, auk, auth_input })
}
#[cfg(test)]
diff --git a/crates/kv-crypto/src/seal.rs b/crates/kv-crypto/src/seal.rs
index 6c0f5d5..495acab 100644
--- a/crates/kv-crypto/src/seal.rs
+++ b/crates/kv-crypto/src/seal.rs
@@ -60,16 +60,23 @@ impl EncKeypair {
/// Seals `plaintext` to a recipient public key (raw 32-byte X25519).
pub fn seal_to(recipient_id: Uuid, recipient_pk: &[u8], plaintext: &[u8], aad: &[u8]) -> Result {
+ seal_to_with_rng(recipient_id, recipient_pk, plaintext, aad, &mut OsRng)
+}
+
+/// Same as [`seal_to`] with a caller-supplied RNG. The first 32 bytes it yields are the
+/// ephemeral `ikmE` of RFC 9180 `DeriveKeyPair`. Only for test vectors.
+#[doc(hidden)]
+pub fn seal_to_with_rng(
+ recipient_id: Uuid,
+ recipient_pk: &[u8],
+ plaintext: &[u8],
+ aad: &[u8],
+ rng: &mut R,
+) -> Result {
let pk = ::PublicKey::from_bytes(recipient_pk).map_err(|_| CryptoError::Malformed)?;
- let (encapped, ct) = hpke::single_shot_seal::(
- &OpModeS::Base,
- &pk,
- INFO,
- plaintext,
- aad,
- &mut OsRng,
- )
- .map_err(|_| CryptoError::Encrypt)?;
+ let (encapped, ct) =
+ hpke::single_shot_seal::(&OpModeS::Base, &pk, INFO, plaintext, aad, rng)
+ .map_err(|_| CryptoError::Encrypt)?;
Ok(Envelope {
v: FORMAT_VERSION,
alg: Alg::HpkeX25519ChaCha,
diff --git a/crates/kv-crypto/tests/vectors.rs b/crates/kv-crypto/tests/vectors.rs
new file mode 100644
index 0000000..f92cf78
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors.rs
@@ -0,0 +1,100 @@
+//! The code must reproduce the published test vectors byte for byte, and the vectors must decode.
+
+use std::path::PathBuf;
+
+use kv_crypto::{aead::SymKey, seal::EncKeypair, secret_key::SecretKey};
+use kv_proto::Envelope;
+use serde_json::Value;
+use uuid::Uuid;
+
+#[path = "../vectors/gen.rs"]
+mod gen;
+
+fn dir() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors")
+}
+
+fn load(name: &str) -> Value {
+ let text = std::fs::read_to_string(dir().join(format!("{name}.json"))).expect("vector file (run gen-vectors)");
+ serde_json::from_str(&text).unwrap()
+}
+
+#[test]
+fn code_reproduces_vector_files() {
+ let spec = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../kestrove-spec/vectors");
+ for (name, doc) in gen::generate() {
+ let rendered = gen::render(&doc);
+ let on_disk = std::fs::read_to_string(dir().join(format!("{name}.json"))).unwrap();
+ assert_eq!(rendered, on_disk, "{name}.json differs from what the code produces");
+ // When the spec repo is checked out next to this one, it must carry the same files.
+ if let Ok(published) = std::fs::read_to_string(spec.join(format!("{name}.json"))) {
+ assert_eq!(published, on_disk, "kestrove-spec/vectors/{name}.json is stale");
+ }
+ }
+ assert_eq!(gen::FILES.len(), gen::generate().len());
+}
+
+#[test]
+fn envelopes_open() {
+ for v in load("envelope")["vectors"].as_array().unwrap() {
+ let key: [u8; 32] = gen::unhex(v["key"].as_str().unwrap()).try_into().unwrap();
+ let key = SymKey::from_bytes(Uuid::parse_str(v["kid"].as_str().unwrap()).unwrap(), key);
+ let env: Envelope = serde_json::from_value(v["envelope"].clone()).unwrap();
+ let pt = key.open(&env, &gen::unhex(v["aad"].as_str().unwrap())).unwrap();
+ assert_eq!(gen::hex(&pt), v["plaintext"].as_str().unwrap());
+ assert_eq!(gen::hex(&env.ct), v["ciphertext"].as_str().unwrap());
+ }
+}
+
+#[test]
+fn hpke_opens() {
+ for v in load("hpke")["vectors"].as_array().unwrap() {
+ let kp = EncKeypair::from_secret_bytes(
+ Uuid::parse_str(v["recipient_kid"].as_str().unwrap()).unwrap(),
+ &gen::unhex(v["recipient_sk"].as_str().unwrap()),
+ )
+ .unwrap();
+ assert_eq!(gen::hex(&kp.public_bytes()), v["recipient_pk"].as_str().unwrap());
+ let env: Envelope = serde_json::from_value(v["envelope"].clone()).unwrap();
+ let pt = kp.open(&env, &gen::unhex(v["aad"].as_str().unwrap())).unwrap();
+ assert_eq!(gen::hex(&pt), v["plaintext"].as_str().unwrap());
+ }
+}
+
+#[test]
+fn secret_keys_parse() {
+ let doc = load("secret_key_format");
+ for v in doc["valid"].as_array().unwrap() {
+ let k = SecretKey::parse(v["display"].as_str().unwrap()).unwrap();
+ assert_eq!(gen::hex(k.expose()), v["raw"].as_str().unwrap());
+ }
+ for v in doc["alternate_inputs"].as_array().unwrap() {
+ let k = SecretKey::parse(v["input"].as_str().unwrap()).unwrap();
+ assert_eq!(gen::hex(k.expose()), v["raw"].as_str().unwrap());
+ }
+ for v in doc["invalid"].as_array().unwrap() {
+ assert!(SecretKey::parse(v["input"].as_str().unwrap()).is_err(), "{v}");
+ }
+}
+
+#[test]
+fn rfc6238_codes_are_the_published_ones() {
+ // Spot-check against the RFC 6238 Appendix B table itself, not only our own output.
+ let doc = load("totp");
+ let find = |t: u64, alg: &str| {
+ doc["vectors"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .find(|v| v["time"] == t && v["uri"].as_str().unwrap().contains(&format!("algorithm={alg}&")))
+ .unwrap()["code"]
+ .as_str()
+ .unwrap()
+ .to_string()
+ };
+ assert_eq!(find(59, "SHA1"), "94287082");
+ assert_eq!(find(59, "SHA256"), "46119246");
+ assert_eq!(find(59, "SHA512"), "90693936");
+ assert_eq!(find(20_000_000_000, "SHA1"), "65353130");
+ assert_eq!(find(20_000_000_000, "SHA512"), "47863826");
+}
diff --git a/crates/kv-crypto/tests/vectors/envelope.json b/crates/kv-crypto/tests/vectors/envelope.json
new file mode 100644
index 0000000..909da69
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors/envelope.json
@@ -0,0 +1,73 @@
+{
+ "aad_layout": "\"kestrove/v1/\" || type || 0x00 || account || 0x00 || vault || 0x00 || item (absent ids = nil UUID)",
+ "account_id": "01010101-0101-4101-8101-010101010101",
+ "description": "XChaCha20-Poly1305 envelopes with location-binding AAD and 256-byte padding (docs/02 'Envelope format')",
+ "item_id": "03030303-0303-4303-8303-030303030303",
+ "vault_id": "02020202-0202-4202-8202-020202020202",
+ "vectors": [
+ {
+ "aad": "6b657374726f76652f76312f6974656d000101010101014101810101010101010100020202020202420282020202020202020003030303030343038303030303030303",
+ "ciphertext": "0ae824b61b2253f40015d22f52ee7412e5be95cf17185bd8490e0e77200d50b5f629c6dfd2e2bccc1d86a06c6453150a96e1c8fc81f1a82b4830f511e831aa66438d35ed9c7d6d11d904b3d53641f05175066f63c0d985ea91738efcd7afb6a205b4a8471fc827a11c51cb598e429227ef2c3020ccdd749f254538ca5429e95f70b7258d87a6b52018976695a15b6e77e6c47a49609fa691f602dbeb4a15ea73c87909c7267d7bf84de31762480feab0b96553c2d4046b5ed00c899e30175fbd7333e9da01329d89a3158a715a85a83d81b8a0105e3c2cdd0ffb2f52e1971d0c7ac1c57504fea274b883fd4b65dae757d758cb3ecd7828e7edd44b35cbba452261039e436beeb3aeba178d379a54abf9",
+ "envelope": {
+ "alg": "xc20p",
+ "ct": "CugkthsiU_QAFdIvUu50EuW-lc8XGFvYSQ4OdyANULX2Kcbf0uK8zB2GoGxkUxUKluHI_IHxqCtIMPUR6DGqZkONNe2cfW0R2QSz1TZB8FF1Bm9jwNmF6pFzjvzXr7aiBbSoRx_IJ6EcUctZjkKSJ-8sMCDM3XSfJUU4ylQp6V9wtyWNh6a1IBiXZpWhW2535sR6SWCfppH2AtvrShXqc8h5CccmfXv4TeMXYkgP6rC5ZVPC1ARrXtAMiZ4wF1-9czPp2gEynYmjFYpxWoWoPYG4oBBePCzdD_svUuGXHQx6wcV1BP6idLiD_Utl2udX11jLPs14KOft1Es1y7pFImEDnkNr7rOuuheNN5pUq_k",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "n": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB",
+ "v": 1
+ },
+ "key": "4242424242424242424242424242424242424242424242424242424242424242",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "name": "item payload (padded)",
+ "nonce": "010101010101010101010101010101010101010101010101",
+ "plaintext": "6d0000007b226964223a2230333033303330332d303330332d343330332d383330332d303330333033303330333033222c226b696e64223a226c6f67696e222c227469746c65223a224578616d706c65222c22637265617465645f6174223a312c226d6f6469666965645f6174223a317d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ },
+ {
+ "aad": "6b657374726f76652f76312f6974656d2d6b6579000101010101014101810101010101010100020202020202420282020202020202020003030303030343038303030303030303",
+ "ciphertext": "89b9440109da7c2a9fa4c5d03a25c7a047b11f9133e7e8ddcff986029675c5106f9240f5ca69911f73dc1106e3f3d32bcc8470e999d6818689daee36907279ec",
+ "envelope": {
+ "alg": "xc20p",
+ "ct": "iblEAQnafCqfpMXQOiXHoEexH5Ez5-jdz_mGApZ1xRBvkkD1ymmRH3PcEQbj89MrzIRw6ZnWgYaJ2u42kHJ57A",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "n": "AgICAgICAgICAgICAgICAgICAgICAgIC",
+ "v": 1
+ },
+ "key": "4242424242424242424242424242424242424242424242424242424242424242",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "name": "item key: kid || key",
+ "nonce": "020202020202020202020202020202020202020202020202",
+ "plaintext": "050505050505450585050505050505055555555555555555555555555555555555555555555555555555555555555555"
+ },
+ {
+ "aad": "6b657374726f76652f76312f7661756c742d6d657461000101010101014101810101010101010100020202020202420282020202020202020000000000000000000000000000000000",
+ "ciphertext": "0ee485ea37c4e9b6f4ea55e7381f12dbd4390a34b4d1342f9043da961bdcd3b47d1986",
+ "envelope": {
+ "alg": "xc20p",
+ "ct": "DuSF6jfE6bb06lXnOB8S29Q5CjS00TQvkEPalhvc07R9GYY",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "n": "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD",
+ "v": 1
+ },
+ "key": "4242424242424242424242424242424242424242424242424242424242424242",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "name": "vault metadata",
+ "nonce": "030303030303030303030303030303030303030303030303",
+ "plaintext": "7b226e616d65223a22506572736f6e616c227d"
+ },
+ {
+ "aad": "6b657374726f76652f76312f6b6579736574000101010101014101810101010101010100000000000000000000000000000000000000000000000000000000000000000000",
+ "ciphertext": "e7b3287dd61ee4062f51d81ed4bc13841733671cfd05429e42f7c820906a73e8d8ca3f083b0713fa560a597c65c18cceccbf90ae2a17e711f8d3a9b7c94ba4561d2e0f5d63bbd4bb4f6d5c81b0d903cad7f19e5a89abb43973269f37f8aad8e5",
+ "envelope": {
+ "alg": "xc20p",
+ "ct": "57MofdYe5AYvUdge1LwThBczZxz9BUKeQvfIIJBqc-jYyj8IOwcT-lYKWXxlwYzOzL-QrioX5xH406m3yUukVh0uD11ju9S7T21cgbDZA8rX8Z5aiau0OXMmnzf4qtjl",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "n": "BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE",
+ "v": 1
+ },
+ "key": "4242424242424242424242424242424242424242424242424242424242424242",
+ "kid": "04040404-0404-4404-8404-040404040404",
+ "name": "keyset (80 bytes)",
+ "nonce": "040404040404040404040404040404040404040404040404",
+ "plaintext": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f"
+ }
+ ]
+}
diff --git a/crates/kv-crypto/tests/vectors/hpke.json b/crates/kv-crypto/tests/vectors/hpke.json
new file mode 100644
index 0000000..c96db69
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors/hpke.json
@@ -0,0 +1,44 @@
+{
+ "account_id": "01010101-0101-4101-8101-010101010101",
+ "description": "HPKE base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305 sealing a vault key (vault key id || key) to the account. ikm_e is the RFC 9180 DeriveKeyPair input for the ephemeral key.",
+ "vectors": [
+ {
+ "aad": "6b657374726f76652f76312f7661756c742d6b6579000101010101014101810101010101010100020202020202420282020202020202020000000000000000000000000000000000",
+ "ciphertext": "41e270319f576cf6e10bd2981b866b9a296b42b810e5b10f617f2b03fabe54e1dbbc434382b79b007dff7734de75e03f2a99cb54ec0d4be64c0972211fa65442",
+ "enc": "e62492d40ab10d0f43fdaa89e1f2107e1e2d059d23a1f00c487a7e62b5c6e67d",
+ "envelope": {
+ "alg": "hpke-x25519-chacha",
+ "ct": "QeJwMZ9XbPbhC9KYG4ZrmilrQrgQ5bEPYX8rA_q-VOHbvENDgrebAH3_dzTedeA_KpnLVOwNS-ZMCXIhH6ZUQg",
+ "enc": "5iSS1AqxDQ9D_aqJ4fIQfh4tBZ0jofAMSHp-YrXG5n0",
+ "kid": "0e0e0e0e-0e0e-4e0e-8e0e-0e0e0e0e0e0e",
+ "v": 1
+ },
+ "ikm_e": "7777777777777777777777777777777777777777777777777777777777777777",
+ "info": "6b657374726f76652f76312f68706b65",
+ "plaintext": "0a0a0a0a0a0a4a0a8a0a0a0a0a0a0a0aabababababababababababababababababababababababababababababababab",
+ "recipient_kid": "0e0e0e0e-0e0e-4e0e-8e0e-0e0e0e0e0e0e",
+ "recipient_pk": "7b4e909bbe7ffe44c465a220037d608ee35897d31ef972f07f74892cb0f73f13",
+ "recipient_sk": "1111111111111111111111111111111111111111111111111111111111111111",
+ "vault_id": "02020202-0202-4202-8202-020202020202"
+ },
+ {
+ "aad": "6b657374726f76652f76312f7661756c742d6b6579000101010101014101810101010101010100090909090909490989090909090909090000000000000000000000000000000000",
+ "ciphertext": "da381938bfd0c81106206bca02b17c5bade147d88e21e6627aeac640c232e489c5a54cf659e27c6b272bbce1df895f998a3ed250b4b299a3d2485a0f05519a4b",
+ "enc": "90ab790ecbf0704232ffe9436faeccc913b66a60e99c688a40bba4d5e2e61450",
+ "envelope": {
+ "alg": "hpke-x25519-chacha",
+ "ct": "2jgZOL_QyBEGIGvKArF8W63hR9iOIeZieurGQMIy5InFpUz2WeJ8aycrvOHfiV-Zij7SULSymaPSSFoPBVGaSw",
+ "enc": "kKt5DsvwcEIy_-lDb67MyRO2amDpnGiKQLuk1eLmFFA",
+ "kid": "0e0e0e0e-0e0e-4e0e-8e0e-0e0e0e0e0e0e",
+ "v": 1
+ },
+ "ikm_e": "0303030303030303030303030303030303030303030303030303030303030303",
+ "info": "6b657374726f76652f76312f68706b65",
+ "plaintext": "0a0a0a0a0a0a4a0a8a0a0a0a0a0a0a0aabababababababababababababababababababababababababababababababab",
+ "recipient_kid": "0e0e0e0e-0e0e-4e0e-8e0e-0e0e0e0e0e0e",
+ "recipient_pk": "07aaff3e9fc167275544f4c3a6a17cd837f2ec6e78cd8a57b1e3dfb3cc035a76",
+ "recipient_sk": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a",
+ "vault_id": "09090909-0909-4909-8909-090909090909"
+ }
+ ]
+}
diff --git a/crates/kv-crypto/tests/vectors/kdf.json b/crates/kv-crypto/tests/vectors/kdf.json
new file mode 100644
index 0000000..898daa3
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors/kdf.json
@@ -0,0 +1,37 @@
+{
+ "description": "2SKD (docs/02 'Derivation'): Argon2id(NFKD(trim(password))) + HKDF(secret_key) -> root -> AUK, AUTH_INPUT",
+ "vectors": [
+ {
+ "account_id": "11111111-1111-4111-9111-111111111111",
+ "auk": "33e9bfb720e3cb5092677bf71d58781f7c032840299d5cc0f10c59b6d0b868d6",
+ "auth_input": "0eeae51fc4c439082b91211477c5e6fd792c98f1781a7b9808dbb39b54483096",
+ "params": {
+ "alg": "argon2id",
+ "m_kib": 65536,
+ "p": 4,
+ "salt": "a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5",
+ "t": 3
+ },
+ "password": "correct horse battery staple",
+ "root": "c6b0513bc1244cdabe9b2ec388b5cd441b8d25a3ba0ab8834f9fb231ad89a5e7",
+ "secret_key": "01010101010101010101010101010101",
+ "secret_key_display": "K1-01040G-20810-40G20-81040-G2081-C"
+ },
+ {
+ "account_id": "22222222-2222-4222-a222-222222222222",
+ "auk": "fc443a82cb371b0a33672509e61b64c08cd26bdc4ce0cdc726e98adc25906068",
+ "auth_input": "77a83f16adf68bae8fe9ee448e7b2ab60ff2ea5234fba69816d5b560700c18a1",
+ "params": {
+ "alg": "argon2id",
+ "m_kib": 65536,
+ "p": 4,
+ "salt": "00000000000000000000000000000000",
+ "t": 3
+ },
+ "password": " café fish ",
+ "root": "da0438d0cb15483dff9f4f569741911fdde9dee37b116c11402480c8f4d9b406",
+ "secret_key": "ffffffffffffffffffffffffffffffff",
+ "secret_key_display": "K1-7ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-T"
+ }
+ ]
+}
diff --git a/crates/kv-crypto/tests/vectors/matching.json b/crates/kv-crypto/tests/vectors/matching.json
new file mode 100644
index 0000000..eeca2bf
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors/matching.json
@@ -0,0 +1,80 @@
+{
+ "description": "Autofill URL matching (docs/05): exact > host > domain (eTLD+1 via the PSL) > none. http items may match https pages, never the reverse.",
+ "vectors": [
+ {
+ "item_url": "https://example.com",
+ "level": "exact",
+ "page_url": "https://example.com/login"
+ },
+ {
+ "item_url": "example.com",
+ "level": "exact",
+ "page_url": "https://example.com/"
+ },
+ {
+ "item_url": "https://example.com:8443",
+ "level": "host",
+ "page_url": "https://example.com"
+ },
+ {
+ "item_url": "https://example.com",
+ "level": "domain",
+ "page_url": "https://login.example.com"
+ },
+ {
+ "item_url": "https://login.example.com",
+ "level": "domain",
+ "page_url": "https://example.com"
+ },
+ {
+ "item_url": "https://a.github.io",
+ "level": "none",
+ "page_url": "https://b.github.io"
+ },
+ {
+ "item_url": "https://example.com",
+ "level": "none",
+ "page_url": "https://example.com.evil.net"
+ },
+ {
+ "item_url": "https://example.com",
+ "level": "none",
+ "page_url": "http://example.com"
+ },
+ {
+ "item_url": "http://example.com",
+ "level": "host",
+ "page_url": "https://example.com"
+ },
+ {
+ "item_url": "https://example.co.uk",
+ "level": "none",
+ "page_url": "https://other.co.uk"
+ },
+ {
+ "item_url": "https://shop.example.co.uk",
+ "level": "domain",
+ "page_url": "https://www.example.co.uk/cart"
+ },
+ {
+ "item_url": "https://EXAMPLE.com",
+ "level": "exact",
+ "page_url": "https://example.COM/"
+ },
+ {
+ "item_url": "https://192.168.1.10:8080",
+ "level": "exact",
+ "page_url": "https://192.168.1.10:8080/admin"
+ },
+ {
+ "item_url": "not a url",
+ "level": "none",
+ "page_url": "https://example.com"
+ },
+ {
+ "item_url": "ftp://example.com",
+ "level": "none",
+ "page_url": "https://example.com"
+ }
+ ]
+}
diff --git a/crates/kv-crypto/tests/vectors/secret_key_format.json b/crates/kv-crypto/tests/vectors/secret_key_format.json
new file mode 100644
index 0000000..d4fcc8a
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors/secret_key_format.json
@@ -0,0 +1,66 @@
+{
+ "alphabet": "0123456789ABCDEFGHJKMNPQRSTVWXYZ",
+ "alternate_inputs": [
+ {
+ "input": "k1-014d2p-f2dbs-qqzxq-5tk1v-58cgg-1",
+ "raw": "0123456789abcdeffedcba9876543210"
+ },
+ {
+ "input": "K1 014D2P F2DBS QQZXQ 5TK1V 58CGG 1",
+ "raw": "0123456789abcdeffedcba9876543210"
+ },
+ {
+ "input": "K1014D2PF2DBSQQZXQ5TK1V58CGG1",
+ "raw": "0123456789abcdeffedcba9876543210"
+ },
+ {
+ "input": "K1-OI4D2P-F2DBS-QQZXQ-5TKIV-58CGG-I",
+ "raw": "0123456789abcdeffedcba9876543210"
+ }
+ ],
+ "description": "Secret Key display format K1-XXXXXX-XXXXX-XXXXX-XXXXX-XXXXX-C: 26 Crockford base32 symbols of the 128-bit key (MSB first, 2 zero pad bits at the top), check symbol = SHA-256(raw)[0] mod 32",
+ "invalid": [
+ {
+ "input": "K1-014D2P-F2DBS-QQZXQ-5TK1V-58CGG-0",
+ "reason": "checksum"
+ },
+ {
+ "input": "K2-014D2P-F2DBS-QQZXQ-5TK1V-58CGG-1",
+ "reason": "prefix"
+ },
+ {
+ "input": "K1-014D2P-F2DBS-QQZXQ-5TK1V-58CGG",
+ "reason": "length"
+ },
+ {
+ "input": "K1-8ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-T",
+ "reason": "overflow: first symbol carries only 3 bits"
+ },
+ {
+ "input": "K1-U014D2P-F2DBS-QQZXQ-5TK1V-58CGG-",
+ "reason": "invalid symbol U"
+ }
+ ],
+ "valid": [
+ {
+ "display": "K1-000000-00000-00000-00000-00000-Q",
+ "raw": "00000000000000000000000000000000"
+ },
+ {
+ "display": "K1-7ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-ZZZZZ-T",
+ "raw": "ffffffffffffffffffffffffffffffff"
+ },
+ {
+ "display": "K1-40G208-1040G-20810-40G20-81040-E",
+ "raw": "80808080808080808080808080808080"
+ },
+ {
+ "display": "K1-5BNENT-QAXBN-ENTQA-XBNEN-TQAXB-T",
+ "raw": "abababababababababababababababab"
+ },
+ {
+ "display": "K1-014D2P-F2DBS-QQZXQ-5TK1V-58CGG-1",
+ "raw": "0123456789abcdeffedcba9876543210"
+ }
+ ]
+}
diff --git a/crates/kv-crypto/tests/vectors/totp.json b/crates/kv-crypto/tests/vectors/totp.json
new file mode 100644
index 0000000..798789c
--- /dev/null
+++ b/crates/kv-crypto/tests/vectors/totp.json
@@ -0,0 +1,131 @@
+{
+ "description": "RFC 6238 TOTP (HMAC-SHA1/256/512). `uri` is what Totp::parse accepts.",
+ "vectors": [
+ {
+ "code": "94287082",
+ "source": "RFC 6238 Appendix B",
+ "time": 59,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30"
+ },
+ {
+ "code": "46119246",
+ "source": "RFC 6238 Appendix B",
+ "time": 59,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&algorithm=SHA256&digits=8&period=30"
+ },
+ {
+ "code": "90693936",
+ "source": "RFC 6238 Appendix B",
+ "time": 59,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA&algorithm=SHA512&digits=8&period=30"
+ },
+ {
+ "code": "07081804",
+ "source": "RFC 6238 Appendix B",
+ "time": 1111111109,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30"
+ },
+ {
+ "code": "68084774",
+ "source": "RFC 6238 Appendix B",
+ "time": 1111111109,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&algorithm=SHA256&digits=8&period=30"
+ },
+ {
+ "code": "25091201",
+ "source": "RFC 6238 Appendix B",
+ "time": 1111111109,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA&algorithm=SHA512&digits=8&period=30"
+ },
+ {
+ "code": "14050471",
+ "source": "RFC 6238 Appendix B",
+ "time": 1111111111,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30"
+ },
+ {
+ "code": "67062674",
+ "source": "RFC 6238 Appendix B",
+ "time": 1111111111,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&algorithm=SHA256&digits=8&period=30"
+ },
+ {
+ "code": "99943326",
+ "source": "RFC 6238 Appendix B",
+ "time": 1111111111,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA&algorithm=SHA512&digits=8&period=30"
+ },
+ {
+ "code": "89005924",
+ "source": "RFC 6238 Appendix B",
+ "time": 1234567890,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30"
+ },
+ {
+ "code": "91819424",
+ "source": "RFC 6238 Appendix B",
+ "time": 1234567890,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&algorithm=SHA256&digits=8&period=30"
+ },
+ {
+ "code": "93441116",
+ "source": "RFC 6238 Appendix B",
+ "time": 1234567890,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA&algorithm=SHA512&digits=8&period=30"
+ },
+ {
+ "code": "69279037",
+ "source": "RFC 6238 Appendix B",
+ "time": 2000000000,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30"
+ },
+ {
+ "code": "90698825",
+ "source": "RFC 6238 Appendix B",
+ "time": 2000000000,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&algorithm=SHA256&digits=8&period=30"
+ },
+ {
+ "code": "38618901",
+ "source": "RFC 6238 Appendix B",
+ "time": 2000000000,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA&algorithm=SHA512&digits=8&period=30"
+ },
+ {
+ "code": "65353130",
+ "source": "RFC 6238 Appendix B",
+ "time": 20000000000,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30"
+ },
+ {
+ "code": "77737706",
+ "source": "RFC 6238 Appendix B",
+ "time": 20000000000,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&algorithm=SHA256&digits=8&period=30"
+ },
+ {
+ "code": "47863826",
+ "source": "RFC 6238 Appendix B",
+ "time": 20000000000,
+ "uri": "otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA&algorithm=SHA512&digits=8&period=30"
+ },
+ {
+ "code": "287082",
+ "source": "bare base32 secret, defaults (SHA1, 6 digits, 30 s)",
+ "time": 59,
+ "uri": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
+ },
+ {
+ "code": "081804",
+ "source": "bare base32 secret, defaults (SHA1, 6 digits, 30 s)",
+ "time": 1111111109,
+ "uri": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
+ },
+ {
+ "code": "857319",
+ "source": "60 s period",
+ "time": 1111111109,
+ "uri": "otpauth://totp/x?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA&digits=6&period=60&algorithm=SHA256"
+ }
+ ]
+}
diff --git a/crates/kv-crypto/vectors/gen.rs b/crates/kv-crypto/vectors/gen.rs
new file mode 100644
index 0000000..4d0e780
--- /dev/null
+++ b/crates/kv-crypto/vectors/gen.rs
@@ -0,0 +1,305 @@
+//! Deterministic test-vector generation (kestrove-spec/vectors). Shared by `examples/gen-vectors.rs`
+//! and `tests/vectors.rs`. Every input is fixed, so regenerating must reproduce the files byte for byte.
+//!
+//! Byte strings are lowercase hex unless noted. Envelopes are given in their JSON wire form (base64url).
+
+#![allow(dead_code)] // each includer uses a different subset
+
+use kv_crypto::{
+ aad::{Aad, ObjectType},
+ aead::{pad, SymKey},
+ kdf,
+ seal::{seal_to_with_rng, EncKeypair},
+ secret_key::SecretKey,
+};
+use kv_model::{matching, totp::Totp};
+use kv_proto::KdfParams;
+use serde_json::{json, Value};
+use uuid::Uuid;
+
+pub const FILES: [&str; 6] = ["kdf", "envelope", "hpke", "secret_key_format", "totp", "matching"];
+
+pub fn hex(b: &[u8]) -> String {
+ b.iter().map(|x| format!("{x:02x}")).collect()
+}
+
+pub fn unhex(s: &str) -> Vec {
+ (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
+}
+
+fn id(n: u8) -> Uuid {
+ // Valid v4 layout so every implementation's UUID parser accepts it.
+ let mut b = [n; 16];
+ b[6] = 0x40 | (n & 0x0f);
+ b[8] = 0x80 | (n & 0x3f);
+ Uuid::from_bytes(b)
+}
+
+/// Returns `(file stem, JSON document)` for every vector file.
+pub fn generate() -> Vec<(&'static str, Value)> {
+ vec![
+ ("kdf", kdf_vectors()),
+ ("envelope", envelope_vectors()),
+ ("hpke", hpke_vectors()),
+ ("secret_key_format", secret_key_vectors()),
+ ("totp", totp_vectors()),
+ ("matching", matching_vectors()),
+ ]
+}
+
+pub fn render(doc: &Value) -> String {
+ let mut s = serde_json::to_string_pretty(doc).unwrap();
+ s.push('\n');
+ s
+}
+
+fn kdf_vectors() -> Value {
+ let cases = [
+ ("correct horse battery staple", [0x01u8; 16], [0xa5u8; 16], id(0x11)),
+ // Leading/trailing whitespace is trimmed and the password NFKD-normalized ("é" precomposed, "fi" ligature).
+ (" caf\u{e9} \u{fb01}sh ", [0xffu8; 16], [0x00u8; 16], id(0x22)),
+ ];
+ let vectors: Vec = cases
+ .iter()
+ .map(|(pw, sk, salt, account)| {
+ let params = KdfParams { alg: "argon2id".into(), m_kib: 64 * 1024, t: 3, p: 4, salt: salt.to_vec() };
+ let secret_key = SecretKey::from_bytes(*sk);
+ let d = kdf::derive_keys(pw, &secret_key, *account, ¶ms).unwrap();
+ json!({
+ "password": pw,
+ "secret_key": hex(sk),
+ "secret_key_display": secret_key.to_display().as_str(),
+ "account_id": account.to_string(),
+ "params": {"alg": "argon2id", "m_kib": params.m_kib, "t": params.t, "p": params.p, "salt": hex(salt)},
+ "root": hex(d.root.as_ref()),
+ "auk": hex(d.auk.as_ref()),
+ "auth_input": hex(d.auth_input.as_ref()),
+ })
+ })
+ .collect();
+ json!({
+ "description": "2SKD (docs/02 'Derivation'): Argon2id(NFKD(trim(password))) + HKDF(secret_key) -> root -> AUK, AUTH_INPUT",
+ "vectors": vectors,
+ })
+}
+
+fn envelope_vectors() -> Value {
+ let account = id(0x01);
+ let vault = id(0x02);
+ let item = id(0x03);
+ let key = SymKey::from_bytes(id(0x04), [0x42; 32]);
+ let item_json = br#"{"id":"03030303-0303-4303-8303-030303030303","kind":"login","title":"Example","created_at":1,"modified_at":1}"#;
+ type Case = (&'static str, ObjectType, Option, Option, Vec, [u8; 24]);
+ let cases: Vec = vec![
+ ("item payload (padded)", ObjectType::Item, Some(vault), Some(item), pad(item_json).to_vec(), [0x01; 24]),
+ (
+ "item key: kid || key",
+ ObjectType::ItemKey,
+ Some(vault),
+ Some(item),
+ [id(0x05).as_bytes().as_slice(), &[0x55; 32]].concat(),
+ [0x02; 24],
+ ),
+ ("vault metadata", ObjectType::VaultMeta, Some(vault), None, br#"{"name":"Personal"}"#.to_vec(), [0x03; 24]),
+ ("keyset (80 bytes)", ObjectType::Keyset, None, None, (0u8..80).collect(), [0x04; 24]),
+ ];
+ let vectors: Vec = cases
+ .into_iter()
+ .map(|(name, ty, v, i, pt, nonce)| {
+ let aad = Aad::build(ty, account, v, i);
+ let env = key.seal_with_nonce(nonce, &pt, &aad).unwrap();
+ json!({
+ "name": name,
+ "key": hex(key.expose()),
+ "kid": key.id.to_string(),
+ "nonce": hex(&nonce),
+ "aad": hex(&aad),
+ "plaintext": hex(&pt),
+ "ciphertext": hex(&env.ct),
+ "envelope": env,
+ })
+ })
+ .collect();
+ json!({
+ "description": "XChaCha20-Poly1305 envelopes with location-binding AAD and 256-byte padding (docs/02 'Envelope format')",
+ "aad_layout": "\"kestrove/v1/\" || type || 0x00 || account || 0x00 || vault || 0x00 || item (absent ids = nil UUID)",
+ "account_id": account.to_string(),
+ "vault_id": vault.to_string(),
+ "item_id": item.to_string(),
+ "vectors": vectors,
+ })
+}
+
+/// Yields fixed bytes: the HPKE ephemeral `ikmE`.
+struct FixedRng(Vec, usize);
+
+impl rand_core::RngCore for FixedRng {
+ fn next_u32(&mut self) -> u32 {
+ let mut b = [0u8; 4];
+ self.fill_bytes(&mut b);
+ u32::from_le_bytes(b)
+ }
+ fn next_u64(&mut self) -> u64 {
+ let mut b = [0u8; 8];
+ self.fill_bytes(&mut b);
+ u64::from_le_bytes(b)
+ }
+ fn fill_bytes(&mut self, dest: &mut [u8]) {
+ for d in dest {
+ *d = self.0[self.1 % self.0.len()];
+ self.1 += 1;
+ }
+ }
+ fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
+ self.fill_bytes(dest);
+ Ok(())
+ }
+}
+
+impl rand_core::CryptoRng for FixedRng {}
+
+fn hpke_vectors() -> Value {
+ let account = id(0x01);
+ let cases = [([0x11u8; 32], [0x77u8; 32], id(0x02)), ([0x2au8; 32], [0x03u8; 32], id(0x09))];
+ let vectors: Vec = cases
+ .iter()
+ .map(|(sk, ikm_e, vault)| {
+ let kp = EncKeypair::from_secret_bytes(id(0x0e), sk).unwrap();
+ let aad = Aad::build(ObjectType::VaultKey, account, Some(*vault), None);
+ let pt = [id(0x0a).as_bytes().as_slice(), &[0xab; 32]].concat();
+ let env = seal_to_with_rng(kp.id, &kp.public_bytes(), &pt, &aad, &mut FixedRng(ikm_e.to_vec(), 0)).unwrap();
+ json!({
+ "recipient_sk": hex(sk),
+ "recipient_pk": hex(&kp.public_bytes()),
+ "recipient_kid": kp.id.to_string(),
+ "ikm_e": hex(ikm_e),
+ "info": hex(b"kestrove/v1/hpke"),
+ "vault_id": vault.to_string(),
+ "aad": hex(&aad),
+ "plaintext": hex(&pt),
+ "enc": hex(&env.enc),
+ "ciphertext": hex(&env.ct),
+ "envelope": env,
+ })
+ })
+ .collect();
+ json!({
+ "description": "HPKE base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305 sealing a vault key (vault key id || key) to the account. ikm_e is the RFC 9180 DeriveKeyPair input for the ephemeral key.",
+ "account_id": account.to_string(),
+ "vectors": vectors,
+ })
+}
+
+fn secret_key_vectors() -> Value {
+ let raws: [[u8; 16]; 5] = [
+ [0x00; 16],
+ [0xff; 16],
+ [0x80; 16],
+ [0xab; 16],
+ *b"\x01\x23\x45\x67\x89\xab\xcd\xef\xfe\xdc\xba\x98\x76\x54\x32\x10",
+ ];
+ let valid: Vec = raws
+ .iter()
+ .map(|r| json!({"raw": hex(r), "display": SecretKey::from_bytes(*r).to_display().as_str()}))
+ .collect();
+ let base = SecretKey::from_bytes(raws[4]).to_display().to_string();
+ let lower = base.to_lowercase();
+ let spaced = base.replace('-', " ");
+ let compact = base.replace('-', "");
+ // Crockford confusables: 0 may be typed as O, 1 as I or L.
+ let confusable = format!("K1{}", base[2..].replace('0', "O").replace('1', "I"));
+ let alternate: Vec = [lower, spaced, compact, confusable]
+ .into_iter()
+ .map(|s| {
+ assert_eq!(SecretKey::parse(&s).unwrap().expose(), &raws[4], "{s}");
+ json!({"input": s, "raw": hex(&raws[4])})
+ })
+ .collect();
+
+ let check = base.chars().last().unwrap();
+ let other_check = if check == '0' { '1' } else { '0' };
+ let mut bad_checksum = base.clone();
+ bad_checksum.pop();
+ bad_checksum.push(other_check);
+ let max = SecretKey::from_bytes([0xff; 16]).to_display().to_string();
+ let invalid: Vec = [
+ (bad_checksum, "checksum"),
+ (base.replacen("K1", "K2", 1), "prefix"),
+ (base[..base.len() - 2].to_string(), "length"),
+ (max.replacen("K1-7", "K1-8", 1), "overflow: first symbol carries only 3 bits"),
+ (base.replacen('-', "-U", 1)[..base.len()].to_string(), "invalid symbol U"),
+ ]
+ .into_iter()
+ .map(|(s, why)| {
+ assert!(SecretKey::parse(&s).is_err(), "{s} should be invalid ({why})");
+ json!({"input": s, "reason": why})
+ })
+ .collect();
+ json!({
+ "description": "Secret Key display format K1-XXXXXX-XXXXX-XXXXX-XXXXX-XXXXX-C: 26 Crockford base32 symbols of the 128-bit key (MSB first, 2 zero pad bits at the top), check symbol = SHA-256(raw)[0] mod 32",
+ "alphabet": "0123456789ABCDEFGHJKMNPQRSTVWXYZ",
+ "valid": valid,
+ "alternate_inputs": alternate,
+ "invalid": invalid,
+ })
+}
+
+fn totp_vectors() -> Value {
+ let b32 = |s: &[u8]| data_encoding::BASE32_NOPAD.encode(s);
+ let seeds: [(&str, &[u8]); 3] = [
+ ("SHA1", b"12345678901234567890"),
+ ("SHA256", b"12345678901234567890123456789012"),
+ ("SHA512", b"1234567890123456789012345678901234567890123456789012345678901234"),
+ ];
+ let mut vectors = Vec::new();
+ for t in [59u64, 1_111_111_109, 1_111_111_111, 1_234_567_890, 2_000_000_000, 20_000_000_000] {
+ for (alg, seed) in seeds {
+ let uri = format!("otpauth://totp/RFC6238?secret={}&algorithm={alg}&digits=8&period=30", b32(seed));
+ let code = Totp::parse(&uri).unwrap().code_at(t);
+ vectors.push(json!({"source": "RFC 6238 Appendix B", "uri": uri, "time": t, "code": code}));
+ }
+ }
+ let defaults = b32(b"12345678901234567890");
+ for t in [59u64, 1_111_111_109] {
+ vectors.push(json!({"source": "bare base32 secret, defaults (SHA1, 6 digits, 30 s)", "uri": defaults, "time": t, "code": Totp::parse(&defaults).unwrap().code_at(t)}));
+ }
+ let custom = format!(
+ "otpauth://totp/x?secret={}&digits=6&period=60&algorithm=SHA256",
+ b32(b"12345678901234567890123456789012")
+ );
+ vectors.push(json!({"source": "60 s period", "uri": custom, "time": 1_111_111_109u64, "code": Totp::parse(&custom).unwrap().code_at(1_111_111_109)}));
+ json!({"description": "RFC 6238 TOTP (HMAC-SHA1/256/512). `uri` is what Totp::parse accepts.", "vectors": vectors})
+}
+
+fn matching_vectors() -> Value {
+ let cases = [
+ ("https://example.com", "https://example.com/login"),
+ ("example.com", "https://example.com/"),
+ ("https://example.com:8443", "https://example.com"),
+ ("https://example.com", "https://login.example.com"),
+ ("https://login.example.com", "https://example.com"),
+ ("https://a.github.io", "https://b.github.io"),
+ ("https://example.com", "https://example.com.evil.net"),
+ ("https://example.com", "http://example.com"),
+ ("http://example.com", "https://example.com"),
+ ("https://example.co.uk", "https://other.co.uk"),
+ ("https://shop.example.co.uk", "https://www.example.co.uk/cart"),
+ ("https://EXAMPLE.com", "https://example.COM/"),
+ ("https://192.168.1.10:8080", "https://192.168.1.10:8080/admin"),
+ ("not a url", "https://example.com"),
+ ("ftp://example.com", "https://example.com"),
+ ];
+ let vectors: Vec = cases
+ .iter()
+ .map(|(item, page)| {
+ let level = match matching::match_url(item, page) {
+ matching::MatchLevel::None => "none",
+ matching::MatchLevel::Domain => "domain",
+ matching::MatchLevel::Host => "host",
+ matching::MatchLevel::Exact => "exact",
+ };
+ json!({"item_url": item, "page_url": page, "level": level})
+ })
+ .collect();
+ json!({"description": "Autofill URL matching (docs/05): exact > host > domain (eTLD+1 via the PSL) > none. http items may match https pages, never the reverse.", "vectors": vectors})
+}
diff --git a/crates/kv-ffi/Cargo.toml b/crates/kv-ffi/Cargo.toml
index e48fa9a..9f9772f 100644
--- a/crates/kv-ffi/Cargo.toml
+++ b/crates/kv-ffi/Cargo.toml
@@ -18,8 +18,18 @@ path = "src/bin/uniffi-bindgen.rs"
kv-proto.workspace = true
kv-crypto.workspace = true
kv-model.workspace = true
+kv-store.workspace = true
+kv-sync.workspace = true
+serde.workspace = true
+hkdf.workspace = true
+sha2.workspace = true
+tokio = { workspace = true }
uuid.workspace = true
thiserror.workspace = true
serde_json.workspace = true
zeroize.workspace = true
-uniffi = { workspace = true, features = ["cli"] }
+uniffi = { workspace = true, features = ["cli", "tokio"] }
+
+[dev-dependencies]
+kv-sync = { workspace = true, features = ["test-server"] }
+tempfile.workspace = true
diff --git a/crates/kv-ffi/src/lib.rs b/crates/kv-ffi/src/lib.rs
index 582c921..6b10a9d 100644
--- a/crates/kv-ffi/src/lib.rs
+++ b/crates/kv-ffi/src/lib.rs
@@ -1,25 +1,34 @@
//! The FFI contract between the core and every platform app (kestrove-spec/docs/07 "Work split").
//! Changing a public item here needs a spec PR.
//!
-//! M0 ships an **in-memory** vault so UI work on Apple, Android and desktop can start
-//! now. M1 swaps the storage for `kv-store` + `kv-sync` behind the same API.
+//! M1: the vault is stored in SQLite through `kv-store` (ciphertext only) and synced through
+//! `kv-sync`. `new_in_memory()` keeps working for previews and tests.
+//!
+//! Network methods are `async` (Swift `async`, Kotlin `suspend`, tokio for Rust callers).
use std::{
- collections::HashMap,
sync::{Arc, Mutex},
time::{SystemTime, UNIX_EPOCH},
};
+use hkdf::Hkdf;
use kv_crypto::{
+ aad::{Aad, ObjectType},
aead::SymKey,
kdf,
keys::{open_item, seal_item, AccountKeyset, SealedItem},
+ random_bytes,
secret_key::SecretKey,
CryptoError,
};
use kv_model::{generator, matching, totp::Totp, Item, ItemKind};
-use kv_proto::{Envelope, KdfParams};
+use kv_proto::{b64, DeviceEnroll, Envelope, ItemRecord, KdfParams, Platform};
+use kv_store::{HistoryEntry, MemoryStore, SqliteStore, Store, StoreError, VaultRow};
+use kv_sync::{ApiClient, LoginOutcome, ServerConfig, SyncContext, SyncError};
+use serde::{Deserialize, Serialize};
+use sha2::Sha256;
use uuid::Uuid;
+use zeroize::Zeroizing;
uniffi::setup_scaffolding!();
@@ -37,6 +46,26 @@ pub enum CoreError {
NotFound,
#[error("internal error: {msg}")]
Internal { msg: String },
+ /// `configure_server` / `set_device_signer` have not been called.
+ #[error("sync server is not configured")]
+ NotConfigured,
+ /// The session expired and the master password is needed to log in again
+ /// (after a biometric unlock the core cannot re-run OPAQUE on its own).
+ #[error("not logged in to the sync server")]
+ NotAuthenticated,
+ /// The device is waiting for approval from an enrolled device.
+ #[error("device approval pending")]
+ PendingApproval,
+ #[error("network error: {msg}")]
+ Network { msg: String },
+ /// The server's TLS key matches none of the pins: possible interception. Never retry silently.
+ #[error("server certificate does not match the pinned keys")]
+ PinMismatch,
+ #[error("server error {status}: {msg}")]
+ Server { status: u16, msg: String },
+ /// The server rolled back or tampered with a vault. Syncing that vault has stopped; alert the user.
+ #[error("integrity alarm on vault {vault_id}: {reason}")]
+ IntegrityAlarm { vault_id: String, reason: String },
}
impl From for CoreError {
@@ -49,8 +78,49 @@ impl From for CoreError {
}
}
+impl From for CoreError {
+ fn from(e: StoreError) -> Self {
+ match e {
+ StoreError::NotFound => CoreError::NotFound,
+ StoreError::Io(msg) => CoreError::Internal { msg },
+ }
+ }
+}
+
+impl From for CoreError {
+ fn from(e: SyncError) -> Self {
+ match e {
+ SyncError::Network(msg) => CoreError::Network { msg },
+ SyncError::PinMismatch => CoreError::PinMismatch,
+ SyncError::Config(msg) => CoreError::Invalid { msg },
+ SyncError::Server { status, code, message } => {
+ CoreError::Server { status, msg: format!("{code}: {message}") }
+ }
+ SyncError::Integrity { vault_id, reason } => {
+ CoreError::IntegrityAlarm { vault_id: vault_id.to_string(), reason }
+ }
+ SyncError::NotAuthenticated => CoreError::NotAuthenticated,
+ SyncError::BadCredentials => CoreError::BadCredentials,
+ SyncError::NotFound => CoreError::NotFound,
+ other => CoreError::Internal { msg: other.to_string() },
+ }
+ }
+}
+
+impl From for CoreError {
+ fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
+ CoreError::Internal { msg: format!("device signer: {}", e.reason) }
+ }
+}
+
type Result = std::result::Result;
+fn internal(e: impl std::fmt::Display) -> CoreError {
+ CoreError::Internal { msg: e.to_string() }
+}
+
+// ---------------------------------------------------------------- DTOs
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum ItemKindDto {
Login,
@@ -67,6 +137,15 @@ pub enum MatchLevelDto {
Exact,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
+pub enum PlatformDto {
+ Ios,
+ Macos,
+ Android,
+ Windows,
+ Linux,
+}
+
#[derive(Debug, Clone, uniffi::Record)]
pub struct ItemSummary {
pub id: String,
@@ -101,40 +180,210 @@ pub struct TotpCode {
pub period: u64,
}
-struct Unlocked {
- keyset: AccountKeyset,
- vault_key: SymKey,
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct ServerInfoDto {
+ pub version: String,
+ pub api: u32,
+ pub setup_open: bool,
+ pub min_client: String,
}
-struct State {
+#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
+pub enum LoginResult {
+ /// Logged in and unlocked.
+ Ok,
+ /// An enrolled device must approve this one (or pass `totp`); call `login` again afterwards.
+ PendingApproval,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
+pub struct SyncReport {
+ pub pulled: u32,
+ pub pushed: u32,
+ /// Items where both sides changed; merged field by field (losing values are in item history).
+ pub merged: u32,
+ /// Items an edit brought back after a delete elsewhere. Tell the user.
+ pub restored_item_ids: Vec,
+ pub vaults_synced: u32,
+ /// Remote vaults not yet trusted (no valid signed manifest).
+ pub vaults_skipped: u32,
+}
+
+/// An enrolled (or pending) device, from `list_devices`.
+#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
+pub struct DeviceDto {
+ pub device_id: String,
+ pub name: String,
+ pub platform: PlatformDto,
+ pub approved: bool,
+ pub revoked: bool,
+ /// True for this device.
+ pub current: bool,
+ pub created_at: String,
+ /// Hex of the first 16 bytes of SHA-256(P-256 public key). Show it on both devices before approving.
+ pub key_fingerprint: String,
+}
+
+/// The device's hardware-backed P-256 key (Secure Enclave, StrongBox, TPM), implemented by each platform.
+/// Used to sign every request (RFC 9421).
+#[uniffi::export(with_foreign)]
+pub trait DeviceSigner: Send + Sync {
+ /// Stable UUID string for this device (generated once, stored by the app).
+ fn device_id(&self) -> String;
+ /// SEC1 uncompressed P-256 public key (65 bytes).
+ fn public_key(&self) -> Vec;
+ /// ECDSA P-256 / SHA-256 over `message`, as raw `r || s` (64 bytes, not DER).
+ fn sign(&self, message: Vec) -> std::result::Result, CoreError>;
+}
+
+struct SignerAdapter {
+ id: Uuid,
+ inner: Arc,
+}
+
+impl kv_sync::DeviceSigner for SignerAdapter {
+ fn device_id(&self) -> Uuid {
+ self.id
+ }
+
+ fn public_key(&self) -> Vec {
+ self.inner.public_key()
+ }
+
+ fn sign(&self, message: &[u8]) -> std::result::Result, SyncError> {
+ self.inner.sign(message.to_vec()).map_err(|e| SyncError::Signer(e.to_string()))
+ }
+}
+
+// ---------------------------------------------------------------- persisted records
+
+const META_ACCOUNT: &str = "account";
+const META_SERVER: &str = "server";
+const META_SESSION: &str = "session";
+
+/// Everything needed to unlock: all public or wrapped (docs/02 "Key hierarchy").
+#[derive(Clone, Serialize, Deserialize)]
+struct AccountRecord {
account_id: Uuid,
kdf: KdfParams,
wrapped_keyset: Envelope,
- vault_id: Uuid,
- sealed_vault_key: Envelope,
- items: HashMap,
+ #[serde(with = "b64")]
+ enc_pk: Vec,
+ #[serde(with = "b64")]
+ sig_pk: Vec,
+ primary_vault: Uuid,
+ #[serde(default)]
+ local_unlock: Option,
+}
+
+/// Keyset (and OPAQUE input) wrapped under the biometric-protected Local Unlock Key.
+#[derive(Clone, Serialize, Deserialize)]
+struct LocalUnlockRecord {
+ keyset: Envelope,
+ #[serde(default)]
+ auth_input: Option,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+struct ServerRecord {
+ url: String,
+ pins: Vec,
+}
+
+#[derive(Clone)]
+struct Unlocked {
+ keyset: Arc,
+ vault_key: Arc,
+ /// OPAQUE password, kept while unlocked so an expired session can be renewed without prompting.
+ auth_input: Option>,
+}
+
+struct Device {
+ signer: Arc,
+ name: String,
+ platform: Platform,
+}
+
+#[derive(Default)]
+struct Inner {
+ account: Option,
unlocked: Option,
+ server: Option,
+ device: Option,
+ api: Option>,
}
/// One account's vault on this device.
#[derive(uniffi::Object)]
pub struct VaultCore {
- state: Mutex