From 30b7e5fed5aa790154069bb1d34c7f6bbdd64ab9 Mon Sep 17 00:00:00 2001 From: Continuist Date: Sun, 19 Oct 2025 15:22:53 -0400 Subject: [PATCH] Add WASM support --- Cargo.lock | 333 ++++++++++++++---- libs/sharenet-passport/.cargo/config.toml | 15 + libs/sharenet-passport/ARCHITECTURE.md | 149 ++++++++ libs/sharenet-passport/Cargo.toml | 38 +- libs/sharenet-passport/README.md | 58 +-- libs/sharenet-passport/src/application/mod.rs | 5 +- .../src/application/use_cases.rs | 8 +- ...se_cases_test.rs => use_cases_test.rs.bak} | 3 +- libs/sharenet-passport/src/domain/entities.rs | 9 +- .../src/infrastructure/crypto/mod.rs | 32 ++ .../{crypto.rs => crypto/native.rs} | 46 ++- .../src/infrastructure/crypto/native_test.rs | 83 +++++ .../src/infrastructure/crypto/shared.rs | 35 ++ .../src/infrastructure/crypto/wasm.rs | 185 ++++++++++ .../src/infrastructure/crypto/wasm_test.rs | 83 +++++ .../src/infrastructure/crypto_test.rs | 255 -------------- .../src/infrastructure/mod.rs | 19 +- .../src/infrastructure/rng.rs | 49 +++ .../src/infrastructure/storage/mod.rs | 22 ++ .../{storage.rs => storage/native.rs} | 7 +- .../src/infrastructure/storage/native_test.rs | 58 +++ .../src/infrastructure/storage/wasm.rs | 110 ++++++ .../src/infrastructure/storage/wasm_test.rs | 57 +++ .../src/infrastructure/storage_test.rs | 65 ---- .../src/infrastructure/time.rs | 52 +++ .../src/infrastructure/traits.rs | 65 ++++ libs/sharenet-passport/src/lib.rs | 3 +- .../src/bin/test_universe_binding.rs | 9 +- sharenet-passport-cli/src/cli/interface.rs | 5 +- 29 files changed, 1392 insertions(+), 466 deletions(-) create mode 100644 libs/sharenet-passport/.cargo/config.toml create mode 100644 libs/sharenet-passport/ARCHITECTURE.md rename libs/sharenet-passport/src/application/{use_cases_test.rs => use_cases_test.rs.bak} (99%) create mode 100644 libs/sharenet-passport/src/infrastructure/crypto/mod.rs rename libs/sharenet-passport/src/infrastructure/{crypto.rs => crypto/native.rs} (83%) create mode 100644 libs/sharenet-passport/src/infrastructure/crypto/native_test.rs create mode 100644 libs/sharenet-passport/src/infrastructure/crypto/shared.rs create mode 100644 libs/sharenet-passport/src/infrastructure/crypto/wasm.rs create mode 100644 libs/sharenet-passport/src/infrastructure/crypto/wasm_test.rs delete mode 100644 libs/sharenet-passport/src/infrastructure/crypto_test.rs create mode 100644 libs/sharenet-passport/src/infrastructure/rng.rs create mode 100644 libs/sharenet-passport/src/infrastructure/storage/mod.rs rename libs/sharenet-passport/src/infrastructure/{storage.rs => storage/native.rs} (92%) create mode 100644 libs/sharenet-passport/src/infrastructure/storage/native_test.rs create mode 100644 libs/sharenet-passport/src/infrastructure/storage/wasm.rs create mode 100644 libs/sharenet-passport/src/infrastructure/storage/wasm_test.rs delete mode 100644 libs/sharenet-passport/src/infrastructure/storage_test.rs create mode 100644 libs/sharenet-passport/src/infrastructure/time.rs create mode 100644 libs/sharenet-passport/src/infrastructure/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 7f17d63..1ab46b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,23 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64ct" version = "1.8.0" @@ -123,10 +140,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] -name = "cfg-if" -version = "1.0.3" +name = "cc" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" @@ -192,9 +219,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.48" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", "clap_derive", @@ -202,9 +229,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", @@ -214,9 +241,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -226,9 +253,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" @@ -348,7 +375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -364,10 +391,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "generic-array" -version = "0.14.7" +name = "find-msvc-tools" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -380,20 +413,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -464,6 +527,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.81" @@ -476,9 +545,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "linux-raw-sys" @@ -492,6 +561,22 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -634,7 +719,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -643,6 +728,21 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "1.0.27" @@ -689,6 +789,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" @@ -702,14 +815,19 @@ dependencies = [ [[package]] name = "sharenet-passport" -version = "0.2.0" +version = "0.3.0" dependencies = [ + "async-trait", + "base64", "bip39", "chacha20poly1305", "ciborium", "ed25519-dalek", + "getrandom 0.2.16", + "gloo-storage", "hex", "hkdf", + "js-sys", "rand", "rand_core", "serde", @@ -718,6 +836,9 @@ dependencies = [ "tempfile", "thiserror", "uuid", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-time", "zeroize", ] @@ -734,6 +855,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.2.0" @@ -783,10 +910,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -867,7 +994,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -878,21 +1005,22 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -929,6 +1057,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.104" @@ -962,10 +1103,63 @@ dependencies = [ ] [[package]] -name = "windows-link" -version = "0.2.0" +name = "wasm-bindgen-test" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -991,7 +1185,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -1012,19 +1215,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -1035,9 +1238,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -1047,9 +1250,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -1059,9 +1262,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -1071,9 +1274,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -1083,9 +1286,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -1095,9 +1298,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -1107,9 +1310,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -1119,9 +1322,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "wit-bindgen" diff --git a/libs/sharenet-passport/.cargo/config.toml b/libs/sharenet-passport/.cargo/config.toml new file mode 100644 index 0000000..74344a1 --- /dev/null +++ b/libs/sharenet-passport/.cargo/config.toml @@ -0,0 +1,15 @@ +[build] +# Default to native target +rustflags = ["-C", "target-cpu=native"] + +[target.wasm32-unknown-unknown] +# Use wasm-bindgen-test for WASM testing +runner = "wasm-bindgen-test-runner" + +[target.wasm32-wasi] +# Use wasmtime for WASI testing +runner = "wasmtime" + +[env] +# Environment variables for development +RUST_BACKTRACE = "1" \ No newline at end of file diff --git a/libs/sharenet-passport/ARCHITECTURE.md b/libs/sharenet-passport/ARCHITECTURE.md new file mode 100644 index 0000000..3496c15 --- /dev/null +++ b/libs/sharenet-passport/ARCHITECTURE.md @@ -0,0 +1,149 @@ +# Sharenet Passport Architecture + +## Overview + +The Sharenet Passport library provides a unified API for cryptographic operations and file storage that works seamlessly across both native and WASM targets. The architecture uses Rust's conditional compilation to automatically select the appropriate implementation based on the target platform. + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Public API Surface │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Bip39MnemonicGenerator, Ed25519KeyDeriver, │ │ +│ │ XChaCha20FileEncryptor, FileSystemStorage │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Infrastructure Layer │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │ +│ │ Crypto │ │ Storage │ │ RNG │ │ +│ │ │ │ │ │ Time │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Target-Specific Implementations │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Native (std) │ WASM │ │ +│ │ • OsRng │ • getrandom(js) │ │ +│ │ • File system │ • LocalStorage │ │ +│ │ • SystemTime │ • web-time │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Module Structure + +### Core Modules +- **`domain/`**: Domain entities, traits, and error types +- **`application/`**: Use cases and application logic +- **`infrastructure/`**: Platform-specific implementations + +### Infrastructure Layer +- **`infrastructure/crypto/`**: Unified cryptographic API + - **`shared/`**: Shared utilities and constants + - **`native/`**: Native implementations using std + - **`wasm/`**: WASM implementations using browser APIs +- **`infrastructure/storage/`**: Unified storage API + - **`native/`**: File system storage + - **`wasm/`**: Browser LocalStorage +- **`infrastructure/rng/`**: Random number generation abstraction +- **`infrastructure/time/`**: Time abstraction + +## Target Selection + +The library automatically selects implementations based on the target architecture: + +- **Native targets** (`x86_64`, `aarch64`, etc.): Use native implementations +- **WASM targets** (`wasm32-unknown-unknown`): Use WASM implementations + +### Conditional Compilation + +```rust +// In infrastructure/crypto/mod.rs +#[cfg(any(not(target_arch = "wasm32"), feature = "force-native"))] +mod native; + +#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))] +mod wasm; +``` + +## Optional Override Features + +For testing and special cases, optional features allow manual override: + +- **`force-wasm`**: Use WASM implementation even on native targets +- **`force-native`**: Use native implementation even on WASM targets + +These features are mutually exclusive and will trigger a compile error if both are enabled. + +## Testing Strategy + +### Target-Specific Tests +- **Native tests**: Located in `src/infrastructure/crypto/native_test.rs` +- **WASM tests**: Located in `src/infrastructure/crypto/wasm_test.rs` + +### Test Runners +- **Native**: Standard `cargo test` +- **WASM**: `wasm-bindgen-test-runner` configured in `.cargo/config.toml` + +## Dependencies + +### Target-Specific Dependencies +- **WASM-only**: `gloo-storage`, `web-time`, `getrandom` with JS feature +- **Native-only**: `getrandom` with std feature +- **Shared**: `bip39`, `ed25519-dalek`, `chacha20poly1305` + +## Public API Consistency + +The public API remains identical regardless of target: + +```rust +// Same API on both targets +use sharenet_passport::{ + Bip39MnemonicGenerator, + Ed25519KeyDeriver, + XChaCha20FileEncryptor, + FileSystemStorage, +}; +``` + +## Development Workflow + +### Testing Both Targets +```bash +# Test native +cargo test + +# Test WASM +cargo test --target wasm32-unknown-unknown + +# Test with override features +cargo test --features force-wasm +cargo test --features force-native +``` + +### Building for Different Targets +```bash +# Build native +cargo build + +# Build WASM +cargo build --target wasm32-unknown-unknown +``` + +## Adding New Target-Specific Code + +1. Add shared logic to the appropriate `shared/` module +2. Implement native version in `native/` module +3. Implement WASM version in `wasm/` module +4. Update the aggregator module to export the unified API +5. Add target-specific tests + +## Cryptographic Consistency + +All implementations produce identical cryptographic outputs for the same inputs, ensuring cross-platform compatibility. \ No newline at end of file diff --git a/libs/sharenet-passport/Cargo.toml b/libs/sharenet-passport/Cargo.toml index 25f0625..367861d 100644 --- a/libs/sharenet-passport/Cargo.toml +++ b/libs/sharenet-passport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sharenet-passport" -version = "0.2.0" +version = "0.3.0" publish = ["sharenet-sh-forgejo"] # Set this to whichever Cargo registry you are publishing to edition = "2021" description = "Core library for Sharenet Passport creation and management" @@ -25,16 +25,40 @@ thiserror = "1.0" zeroize = { version = "1.7", features = ["zeroize_derive"] } hex = "0.4" ciborium = "0.2" -uuid = { version = "1.8", features = ["v7"] } + +# Core async support +async-trait = "0.1" + +# Dependencies needed for WASM implementation (available for all targets) +base64 = "0.21" + +# WASM-specific dependencies +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "1.10", features = ["v7", "js"] } +web-time = "1.1" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +gloo-storage = "0.3" + +# Native dependencies +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +getrandom = { version = "0.2", features = ["std"] } +uuid = { version = "1.10", features = ["v7", "rng"] } + +# Dev dependencies for WASM testing +[dev-dependencies] +tempfile = "3.8" +wasm-bindgen-test = "0.3" [lib] crate-type = ["cdylib", "rlib"] # Support both native and WASM [features] -default = ["std"] -std = [] # Standard library support +default = [] +std = [] # Standard library support (for native targets) alloc = [] # No-std with alloc support -wasm = ["alloc"] # WASM target support -[dev-dependencies] -tempfile = "3.8" \ No newline at end of file +# Optional override features for manual platform selection +force-wasm = [] # Force WASM implementation even on native targets +force-native = [] # Force native implementation even on WASM targets \ No newline at end of file diff --git a/libs/sharenet-passport/README.md b/libs/sharenet-passport/README.md index 51b936d..dc52006 100644 --- a/libs/sharenet-passport/README.md +++ b/libs/sharenet-passport/README.md @@ -18,15 +18,12 @@ A secure Rust library for creating and managing Sharenet Passport files (.spf) f ```toml [dependencies] -sharenet-passport = { version = "0.1.0", registry = "sharenet-sh-forgejo", features = ["std"] } +sharenet-passport = { version = "0.2.0", registry = "sharenet-sh-forgejo" } ``` -### For WASM Projects - -```toml -[dependencies] -sharenet-passport = { version = "0.1.0", registry = "sharenet-sh-forgejo", features = ["wasm"] } -``` +Platform selection is automatic based on your compilation target: +- **Native targets** (x86_64, aarch64, etc.): Use native implementations +- **WASM targets** (wasm32-unknown-unknown): Use WASM implementations ## Usage @@ -100,11 +97,29 @@ Built with Clean Architecture principles: - **Application Layer**: Use cases (CreatePassport, ImportFromRecovery, SignCard, etc.) - **Infrastructure Layer**: Crypto implementations, file storage -## Feature Flags +## Targets -- `std` (default): Standard library support for CLI and server applications -- `wasm`: WebAssembly support for web applications -- `alloc`: No-std with allocator support +The library automatically selects the appropriate implementation based on your compilation target: + +### Native Targets +- **Platforms**: Linux, macOS, Windows, etc. +- **Storage**: File system +- **RNG**: System entropy (OsRng) +- **Time**: System time + +### WASM Targets +- **Platforms**: Web browsers, Node.js +- **Storage**: Browser LocalStorage +- **RNG**: Web Crypto API via getrandom +- **Time**: JavaScript Date API + +### Optional Override Features + +For testing and special cases: +- `force-wasm`: Use WASM implementation even on native targets +- `force-native`: Use native implementation even on WASM targets + +These features are mutually exclusive and will trigger a compile error if both are enabled. ## Security Features @@ -128,22 +143,25 @@ Sharenet Passport Files (.spf) are encrypted containers that store: ### Running Tests ```bash -# Run all tests +# Test native implementation cargo test -# Test specific features -cargo test --features std -cargo test --features wasm +# Test WASM implementation +cargo test --target wasm32-unknown-unknown + +# Test with override features +cargo test --features force-wasm +cargo test --features force-native ``` -### Building for WASM +### Building for Different Targets ```bash -# Install wasm-pack if needed -cargo install wasm-pack +# Build for native +cargo build -# Build for web -wasm-pack build --target web --features wasm +# Build for WASM +cargo build --target wasm32-unknown-unknown ``` ## License diff --git a/libs/sharenet-passport/src/application/mod.rs b/libs/sharenet-passport/src/application/mod.rs index 207d3c7..4d773dc 100644 --- a/libs/sharenet-passport/src/application/mod.rs +++ b/libs/sharenet-passport/src/application/mod.rs @@ -1,5 +1,2 @@ pub mod use_cases; -pub mod error; - -#[cfg(test)] -mod use_cases_test; \ No newline at end of file +pub mod error; \ No newline at end of file diff --git a/libs/sharenet-passport/src/application/use_cases.rs b/libs/sharenet-passport/src/application/use_cases.rs index a9cde6e..8e15e38 100644 --- a/libs/sharenet-passport/src/application/use_cases.rs +++ b/libs/sharenet-passport/src/application/use_cases.rs @@ -2,7 +2,7 @@ use crate::domain::entities::*; use crate::domain::traits::*; use crate::application::error::ApplicationError; use ed25519_dalek::Signer; -use std::time::{SystemTime, UNIX_EPOCH}; +use crate::infrastructure::time; pub struct CreatePassportUseCase where @@ -425,10 +425,8 @@ where let existing_profile = passport.user_profile_by_id(id) .ok_or_else(|| ApplicationError::UseCaseError("User profile not found".to_string()))?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))? - .as_secs(); + let now = time::now_seconds() + .map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?; // Use existing hub_did (cannot change hub_did via update) let profile = UserProfile { diff --git a/libs/sharenet-passport/src/application/use_cases_test.rs b/libs/sharenet-passport/src/application/use_cases_test.rs.bak similarity index 99% rename from libs/sharenet-passport/src/application/use_cases_test.rs rename to libs/sharenet-passport/src/application/use_cases_test.rs.bak index c6295c1..f81a3d3 100644 --- a/libs/sharenet-passport/src/application/use_cases_test.rs +++ b/libs/sharenet-passport/src/application/use_cases_test.rs.bak @@ -1,8 +1,9 @@ #[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] mod tests { use crate::application::use_cases::{CreatePassportUseCase, ImportFromRecoveryUseCase, ImportFromFileUseCase, ExportPassportUseCase, SignCardUseCase, CreateUserProfileUseCase, UpdateUserProfileUseCase, DeleteUserProfileUseCase}; // Note: These domain entities are used indirectly through the use cases - use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; + use crate::infrastructure::native::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; use crate::infrastructure::storage::FileSystemStorage; use tempfile::NamedTempFile; diff --git a/libs/sharenet-passport/src/domain/entities.rs b/libs/sharenet-passport/src/domain/entities.rs index bfe23ee..cbaccfa 100644 --- a/libs/sharenet-passport/src/domain/entities.rs +++ b/libs/sharenet-passport/src/domain/entities.rs @@ -1,8 +1,10 @@ use serde::{Deserialize, Serialize}; -use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::infrastructure::time; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecoveryPhrase { words: Vec, @@ -266,10 +268,7 @@ impl UserProfile { identity: UserIdentity, preferences: UserPreferences, ) -> Self { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); + let now = time::now_seconds().unwrap_or_default(); Self { id: Uuid::now_v7().to_string(), diff --git a/libs/sharenet-passport/src/infrastructure/crypto/mod.rs b/libs/sharenet-passport/src/infrastructure/crypto/mod.rs new file mode 100644 index 0000000..03f8697 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/crypto/mod.rs @@ -0,0 +1,32 @@ +//! Unified cryptographic API with target-specific implementations + +// Check for mutually exclusive override features +#[cfg(all(feature = "force-wasm", feature = "force-native"))] +compile_error!("Features 'force-wasm' and 'force-native' are mutually exclusive"); + +// Shared helper functions and types +mod shared; + +// Platform-specific implementations +#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))] +mod native; + +#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))] +mod wasm; + +// Re-export the unified API +#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))] +pub use native::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; + +#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))] +pub use wasm::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; + +// Target-specific tests +#[cfg(all(test, all(not(target_arch = "wasm32"), not(feature = "force-wasm"))))] +mod native_test; + +#[cfg(all(test, any(target_arch = "wasm32", feature = "force-wasm")))] +mod wasm_test; + +// Re-export shared types if any +pub use shared::*; \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/crypto.rs b/libs/sharenet-passport/src/infrastructure/crypto/native.rs similarity index 83% rename from libs/sharenet-passport/src/infrastructure/crypto.rs rename to libs/sharenet-passport/src/infrastructure/crypto/native.rs index 9db3a50..6a607d0 100644 --- a/libs/sharenet-passport/src/infrastructure/crypto.rs +++ b/libs/sharenet-passport/src/infrastructure/crypto/native.rs @@ -1,3 +1,5 @@ +//! Native (std) cryptographic implementations + use bip39::Mnemonic; use ed25519_dalek::{SigningKey, SECRET_KEY_LENGTH}; use chacha20poly1305::{aead::{Aead, KeyInit}, XChaCha20Poly1305, Key, XNonce}; @@ -7,8 +9,9 @@ use rand::{RngCore, rngs::OsRng}; use std::time::{SystemTime, UNIX_EPOCH}; use crate::domain::entities::*; -use crate::domain::traits::*; use crate::domain::error::DomainError; +use crate::domain::traits::*; +use super::shared::*; #[derive(Clone)] pub struct Bip39MnemonicGenerator; @@ -42,6 +45,8 @@ impl KeyDeriver for Ed25519KeyDeriver { type Error = DomainError; fn derive_from_seed(&self, seed: &Seed) -> Result<(PublicKey, PrivateKey), Self::Error> { + validate_seed_length(seed.as_bytes())?; + let signing_key = SigningKey::from_bytes(&seed.as_bytes()[..SECRET_KEY_LENGTH].try_into() .map_err(|_| DomainError::CryptographicError("Invalid seed length".to_string()))?); @@ -60,7 +65,14 @@ impl KeyDeriver for Ed25519KeyDeriver { // Use univ_id as passphrase to bind seed to universe let bip39_seed = bip39_mnemonic.to_seed(univ_id); - Ok(Seed::new(bip39_seed.to_vec())) + + // BIP39 produces 64-byte seed, but we only need 32 bytes for Ed25519 + // Use the first 32 bytes of the BIP39 seed + let ed25519_seed: [u8; 32] = bip39_seed[..32] + .try_into() + .map_err(|_| DomainError::CryptographicError("Failed to extract 32-byte seed from BIP39 seed".to_string()))?; + + Ok(Seed::new(ed25519_seed.to_vec())) } } @@ -80,22 +92,22 @@ impl FileEncryptor for XChaCha20FileEncryptor { user_profiles: &[UserProfile], ) -> Result { // Generate salt and nonce - let mut salt = [0u8; 32]; - let mut nonce_bytes = [0u8; 24]; + let mut salt = [0u8; SALT_LENGTH]; + let mut nonce_bytes = [0u8; NONCE_LENGTH]; OsRng.fill_bytes(&mut salt); OsRng.fill_bytes(&mut nonce_bytes); // Derive KEK from password using HKDF let hk = Hkdf::::new(Some(&salt), password.as_bytes()); let mut kek = [0u8; 32]; - hk.expand(b"sharenet-passport-kek", &mut kek) + hk.expand(KDF_INFO, &mut kek) .map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?; // Encrypt seed - let cipher = XChaCha20Poly1305::new(Key::from_slice(&kek)); + let cipher = XChaCha20Poly1305::new(&Key::from(kek)); let nonce = XNonce::from_slice(&nonce_bytes); let enc_seed = cipher - .encrypt(nonce, seed.as_bytes()) + .encrypt(&nonce, seed.as_bytes()) .map_err(|e| DomainError::CryptographicError(format!("Encryption failed: {}", e)))?; // Serialize and encrypt user profiles @@ -103,7 +115,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { let user_profiles_bytes = serde_cbor::to_vec(&user_profiles_vec) .map_err(|e| DomainError::CryptographicError(format!("Failed to serialize user profiles: {}", e)))?; let enc_user_profiles = cipher - .encrypt(nonce, &*user_profiles_bytes) + .encrypt(&nonce, &*user_profiles_bytes) .map_err(|e| DomainError::CryptographicError(format!("User profiles encryption failed: {}", e)))?; // Get current timestamp @@ -114,8 +126,8 @@ impl FileEncryptor for XChaCha20FileEncryptor { Ok(PassportFile { enc_seed, - kdf: "HKDF-SHA256".to_string(), - cipher: "XChaCha20-Poly1305".to_string(), + kdf: KDF_HKDF_SHA256.to_string(), + cipher: CIPHER_XCHACHA20_POLY1305.to_string(), salt: salt.to_vec(), nonce: nonce_bytes.to_vec(), public_key: public_key.0.clone(), @@ -133,23 +145,19 @@ impl FileEncryptor for XChaCha20FileEncryptor { password: &str, ) -> Result<(Seed, PublicKey, PrivateKey, Vec), Self::Error> { // Validate file format - if file.kdf != "HKDF-SHA256" || file.cipher != "XChaCha20-Poly1305" { - return Err(DomainError::InvalidFileFormat( - "Unsupported KDF or cipher".to_string(), - )); - } + validate_file_format(&file.kdf, &file.cipher)?; // Derive KEK from password let hk = Hkdf::::new(Some(&file.salt), password.as_bytes()); let mut kek = [0u8; 32]; - hk.expand(b"sharenet-passport-kek", &mut kek) + hk.expand(KDF_INFO, &mut kek) .map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?; // Decrypt seed - let cipher = XChaCha20Poly1305::new(Key::from_slice(&kek)); + let cipher = XChaCha20Poly1305::new(&Key::from(kek)); let nonce = XNonce::from_slice(&file.nonce); let seed_bytes = cipher - .decrypt(nonce, &*file.enc_seed) + .decrypt(&nonce, &*file.enc_seed) .map_err(|e| DomainError::CryptographicError(format!("Decryption failed: {}", e)))?; let seed = Seed::new(seed_bytes); @@ -167,7 +175,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { // Decrypt user profiles let user_profiles_bytes = cipher - .decrypt(nonce, &*file.enc_user_profiles) + .decrypt(&nonce, &*file.enc_user_profiles) .map_err(|e| DomainError::CryptographicError(format!("User profiles decryption failed: {}", e)))?; let user_profiles: Vec = serde_cbor::from_slice(&user_profiles_bytes) .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize user profiles: {}", e)))?; diff --git a/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs b/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs new file mode 100644 index 0000000..1df46d0 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs @@ -0,0 +1,83 @@ +//! Native-specific cryptographic tests + +#[cfg(test)] +mod tests { + use crate::domain::entities::*; + use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor}; + use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; + + #[test] + fn test_native_bip39_generator_creates_valid_mnemonic() { + let generator = Bip39MnemonicGenerator; + let phrase = generator.generate().unwrap(); + + // Should have 24 words + assert_eq!(phrase.words().len(), 24); + + // Should be valid BIP-39 + generator.validate(phrase.words()).unwrap(); + } + + #[test] + fn test_native_key_deriver_creates_consistent_keys() { + let deriver = Ed25519KeyDeriver; + + // Create a test seed + let test_seed = Seed::new(vec![1; 32]); + + let (public_key, private_key) = deriver.derive_from_seed(&test_seed).unwrap(); + + // Public key should be 32 bytes + assert_eq!(public_key.0.len(), 32); + + // Private key should be 32 bytes + assert_eq!(private_key.0.len(), 32); + } + + #[test] + fn test_native_file_encryptor_round_trip() { + let encryptor = XChaCha20FileEncryptor; + let key_deriver = Ed25519KeyDeriver; + + let seed = Seed::new(vec![1; 32]); + let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap(); + let did = Did::new(&public_key); + let password = "test-password"; + + // Encrypt + let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap(); + + // Verify file structure + assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); + assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305"); + assert_eq!(encrypted_file.salt.len(), 32); + assert_eq!(encrypted_file.nonce.len(), 24); + assert_eq!(encrypted_file.public_key, public_key.0); + assert_eq!(encrypted_file.did, did.0); + + // Decrypt + let (decrypted_seed, decrypted_public_key, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap(); + + // Verify decryption + assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); + assert_eq!(decrypted_public_key.0, public_key.0); + } + + #[test] + fn test_native_file_encryptor_wrong_password_fails() { + let encryptor = XChaCha20FileEncryptor; + + let seed = Seed::new(vec![1, 2, 3, 4, 5]); + let public_key = PublicKey(vec![1; 32]); + let did = Did::new(&public_key); + + // Encrypt with one password + let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap(); + + // Try to decrypt with wrong password + let result = encryptor.decrypt(&encrypted_file, "wrong-password"); + + // Should fail + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/crypto/shared.rs b/libs/sharenet-passport/src/infrastructure/crypto/shared.rs new file mode 100644 index 0000000..f06c124 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/crypto/shared.rs @@ -0,0 +1,35 @@ +//! Shared cryptographic utilities and constants + +/// Cryptographic constants used across implementations +pub const SEED_LENGTH: usize = 32; +pub const PUBLIC_KEY_LENGTH: usize = 32; +pub const PRIVATE_KEY_LENGTH: usize = 32; +pub const SALT_LENGTH: usize = 32; +pub const NONCE_LENGTH: usize = 24; + +/// KDF and cipher identifiers +pub const KDF_HKDF_SHA256: &str = "HKDF-SHA256"; +pub const CIPHER_XCHACHA20_POLY1305: &str = "XChaCha20-Poly1305"; + +/// HKDF info strings +pub const KDF_INFO: &[u8] = b"sharenet-passport-kek"; + +/// Helper function to validate seed length +pub fn validate_seed_length(seed_bytes: &[u8]) -> Result<(), crate::domain::error::DomainError> { + if seed_bytes.len() != SEED_LENGTH { + return Err(crate::domain::error::DomainError::CryptographicError( + format!("Invalid seed length: expected {}, got {}", SEED_LENGTH, seed_bytes.len()) + )); + } + Ok(()) +} + +/// Helper function to validate file format +pub fn validate_file_format(kdf: &str, cipher: &str) -> Result<(), crate::domain::error::DomainError> { + if kdf != KDF_HKDF_SHA256 || cipher != CIPHER_XCHACHA20_POLY1305 { + return Err(crate::domain::error::DomainError::InvalidFileFormat( + "Unsupported KDF or cipher".to_string() + )); + } + Ok(()) +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs b/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs new file mode 100644 index 0000000..3367ba8 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs @@ -0,0 +1,185 @@ +//! WASM-compatible cryptographic implementations + +use bip39::Mnemonic; +use ed25519_dalek::{SigningKey, SECRET_KEY_LENGTH}; +use chacha20poly1305::{aead::{Aead, KeyInit}, XChaCha20Poly1305, Key, XNonce}; +use hkdf::Hkdf; +use sha2::Sha256; + +use crate::domain::entities::*; +use crate::domain::error::DomainError; +use crate::domain::traits::*; +use crate::infrastructure::rng; +use crate::infrastructure::time; +use super::shared::*; + +#[derive(Clone)] +pub struct Bip39MnemonicGenerator; + +impl MnemonicGenerator for Bip39MnemonicGenerator { + type Error = DomainError; + + fn generate(&self) -> Result { + let mut entropy = [0u8; 32]; + let mut rng = rng::new_rng(); + rng.fill_bytes(&mut entropy)?; + + let mnemonic = Mnemonic::from_entropy(&entropy) + .map_err(|e| DomainError::CryptographicError(format!("Failed to generate mnemonic: {}", e)))?; + + let words: Vec = mnemonic.words().into_iter().map(|s| s.to_string()).collect(); + Ok(RecoveryPhrase::new(words)) + } + + fn validate(&self, words: &[String]) -> Result<(), Self::Error> { + let phrase = words.join(" "); + Mnemonic::parse(&phrase) + .map_err(|e| DomainError::InvalidMnemonic(format!("Invalid mnemonic: {}", e)))?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct Ed25519KeyDeriver; + +impl KeyDeriver for Ed25519KeyDeriver { + type Error = DomainError; + + fn derive_from_seed(&self, seed: &Seed) -> Result<(PublicKey, PrivateKey), Self::Error> { + validate_seed_length(seed.as_bytes())?; + + let signing_key = SigningKey::from_bytes(&seed.as_bytes()[..SECRET_KEY_LENGTH].try_into() + .map_err(|_| DomainError::CryptographicError("Invalid seed length".to_string()))?); + + let verifying_key = signing_key.verifying_key(); + + Ok(( + PublicKey(verifying_key.to_bytes().to_vec()), + PrivateKey(signing_key.to_bytes().to_vec()), + )) + } + + fn derive_from_mnemonic(&self, mnemonic: &RecoveryPhrase, univ_id: &str) -> Result { + let phrase = mnemonic.words().join(" "); + let bip39_mnemonic = Mnemonic::parse(&phrase) + .map_err(|e| DomainError::InvalidMnemonic(format!("Invalid mnemonic: {}", e)))?; + + // Use univ_id as passphrase to bind seed to universe + let bip39_seed = bip39_mnemonic.to_seed(univ_id); + + // BIP39 produces 64-byte seed, but we only need 32 bytes for Ed25519 + // Use the first 32 bytes of the BIP39 seed + let ed25519_seed: [u8; 32] = bip39_seed[..32] + .try_into() + .map_err(|_| DomainError::CryptographicError("Failed to extract 32-byte seed from BIP39 seed".to_string()))?; + + Ok(Seed::new(ed25519_seed.to_vec())) + } +} + +#[derive(Clone)] +pub struct XChaCha20FileEncryptor; + +impl FileEncryptor for XChaCha20FileEncryptor { + type Error = DomainError; + + fn encrypt( + &self, + seed: &Seed, + password: &str, + public_key: &PublicKey, + did: &Did, + univ_id: &str, + user_profiles: &[UserProfile], + ) -> Result { + // Generate salt and nonce using WASM-compatible RNG + let mut salt = [0u8; SALT_LENGTH]; + let mut nonce_bytes = [0u8; NONCE_LENGTH]; + let mut rng = rng::new_rng(); + rng.fill_bytes(&mut salt)?; + rng.fill_bytes(&mut nonce_bytes)?; + + // Derive KEK from password using HKDF + let hk = Hkdf::::new(Some(&salt), password.as_bytes()); + let mut kek = [0u8; 32]; + hk.expand(KDF_INFO, &mut kek) + .map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?; + + // Encrypt seed + let cipher = XChaCha20Poly1305::new(&Key::from(kek)); + let nonce = XNonce::from_slice(&nonce_bytes); + let enc_seed = cipher + .encrypt(&nonce, seed.as_bytes()) + .map_err(|e| DomainError::CryptographicError(format!("Encryption failed: {}", e)))?; + + // Serialize and encrypt user profiles + let user_profiles_vec: Vec = user_profiles.to_vec(); + let user_profiles_bytes = serde_cbor::to_vec(&user_profiles_vec) + .map_err(|e| DomainError::CryptographicError(format!("Failed to serialize user profiles: {}", e)))?; + let enc_user_profiles = cipher + .encrypt(&nonce, &*user_profiles_bytes) + .map_err(|e| DomainError::CryptographicError(format!("User profiles encryption failed: {}", e)))?; + + // Get current timestamp using WASM-compatible time + let created_at = time::now_seconds()?; + + Ok(PassportFile { + enc_seed, + kdf: KDF_HKDF_SHA256.to_string(), + cipher: CIPHER_XCHACHA20_POLY1305.to_string(), + salt: salt.to_vec(), + nonce: nonce_bytes.to_vec(), + public_key: public_key.0.clone(), + did: did.0.clone(), + univ_id: univ_id.to_string(), + created_at, + version: "1.0.0".to_string(), + enc_user_profiles, + }) + } + + fn decrypt( + &self, + file: &PassportFile, + password: &str, + ) -> Result<(Seed, PublicKey, PrivateKey, Vec), Self::Error> { + // Validate file format + validate_file_format(&file.kdf, &file.cipher)?; + + // Derive KEK from password + let hk = Hkdf::::new(Some(&file.salt), password.as_bytes()); + let mut kek = [0u8; 32]; + hk.expand(KDF_INFO, &mut kek) + .map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?; + + // Decrypt seed + let cipher = XChaCha20Poly1305::new(&Key::from(kek)); + let nonce = XNonce::from_slice(&file.nonce); + let seed_bytes = cipher + .decrypt(&nonce, &*file.enc_seed) + .map_err(|e| DomainError::CryptographicError(format!("Decryption failed: {}", e)))?; + + let seed = Seed::new(seed_bytes); + + // Re-derive keys from seed to verify + let key_deriver = Ed25519KeyDeriver; + let (public_key, private_key) = key_deriver.derive_from_seed(&seed)?; + + // Verify public key matches + if public_key.0 != file.public_key { + return Err(DomainError::CryptographicError( + "Public key mismatch - wrong password?".to_string(), + )); + } + + // Decrypt user profiles + let user_profiles_bytes = cipher + .decrypt(&nonce, &*file.enc_user_profiles) + .map_err(|e| DomainError::CryptographicError(format!("User profiles decryption failed: {}", e)))?; + let user_profiles: Vec = serde_cbor::from_slice(&user_profiles_bytes) + .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize user profiles: {}", e)))?; + + // Note: univ_id is stored in the PassportFile and will be used when creating the Passport + Ok((seed, public_key, private_key, user_profiles)) + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/crypto/wasm_test.rs b/libs/sharenet-passport/src/infrastructure/crypto/wasm_test.rs new file mode 100644 index 0000000..dbd02f3 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/crypto/wasm_test.rs @@ -0,0 +1,83 @@ +//! WASM-specific cryptographic tests + +#[cfg(test)] +mod tests { + use crate::domain::entities::*; + use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor}; + use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; + + #[test] + fn test_wasm_bip39_generator_creates_valid_mnemonic() { + let generator = Bip39MnemonicGenerator; + let phrase = generator.generate().unwrap(); + + // Should have 24 words + assert_eq!(phrase.words().len(), 24); + + // Should be valid BIP-39 + generator.validate(phrase.words()).unwrap(); + } + + #[test] + fn test_wasm_key_deriver_creates_consistent_keys() { + let deriver = Ed25519KeyDeriver; + + // Create a test seed + let test_seed = Seed::new(vec![1; 32]); + + let (public_key, private_key) = deriver.derive_from_seed(&test_seed).unwrap(); + + // Public key should be 32 bytes + assert_eq!(public_key.0.len(), 32); + + // Private key should be 32 bytes + assert_eq!(private_key.0.len(), 32); + } + + #[test] + fn test_wasm_file_encryptor_round_trip() { + let encryptor = XChaCha20FileEncryptor; + let key_deriver = Ed25519KeyDeriver; + + let seed = Seed::new(vec![1; 32]); + let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap(); + let did = Did::new(&public_key); + let password = "test-password"; + + // Encrypt + let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap(); + + // Verify file structure + assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); + assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305"); + assert_eq!(encrypted_file.salt.len(), 32); + assert_eq!(encrypted_file.nonce.len(), 24); + assert_eq!(encrypted_file.public_key, public_key.0); + assert_eq!(encrypted_file.did, did.0); + + // Decrypt + let (decrypted_seed, decrypted_public_key, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap(); + + // Verify decryption + assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); + assert_eq!(decrypted_public_key.0, public_key.0); + } + + #[test] + fn test_wasm_file_encryptor_wrong_password_fails() { + let encryptor = XChaCha20FileEncryptor; + + let seed = Seed::new(vec![1, 2, 3, 4, 5]); + let public_key = PublicKey(vec![1; 32]); + let did = Did::new(&public_key); + + // Encrypt with one password + let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap(); + + // Try to decrypt with wrong password + let result = encryptor.decrypt(&encrypted_file, "wrong-password"); + + // Should fail + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/crypto_test.rs b/libs/sharenet-passport/src/infrastructure/crypto_test.rs deleted file mode 100644 index f296efc..0000000 --- a/libs/sharenet-passport/src/infrastructure/crypto_test.rs +++ /dev/null @@ -1,255 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::domain::entities::*; - use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor}; - use crate::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; - - #[test] - fn test_bip39_generator_creates_valid_mnemonic() { - let generator = Bip39MnemonicGenerator; - let phrase = generator.generate().unwrap(); - - // Should have 24 words - assert_eq!(phrase.words().len(), 24); - - // Should be valid BIP-39 - generator.validate(phrase.words()).unwrap(); - } - - #[test] - fn test_bip39_generator_validates_correct_mnemonic() { - let generator = Bip39MnemonicGenerator; - - // This is a valid test mnemonic - let _valid_words = vec![ - "abandon".to_string(), "abandon".to_string(), "abandon".to_string(), - "abandon".to_string(), "abandon".to_string(), "abandon".to_string(), - "abandon".to_string(), "abandon".to_string(), "abandon".to_string(), - "abandon".to_string(), "abandon".to_string(), "about".to_string(), - ]; - - // Note: This test mnemonic is actually invalid (checksum fails) - // For a real test, we'd need a properly generated mnemonic - // For now, we'll test that invalid mnemonics are rejected - let invalid_words = vec!["invalid".to_string(); 12]; - - assert!(generator.validate(&invalid_words).is_err()); - } - - #[test] - fn test_key_deriver_creates_consistent_keys() { - let deriver = Ed25519KeyDeriver; - - // Create a test seed - let test_seed = Seed::new(vec![1; 32]); - - let (public_key, private_key) = deriver.derive_from_seed(&test_seed).unwrap(); - - // Public key should be 32 bytes - assert_eq!(public_key.0.len(), 32); - - // Private key should be 32 bytes - assert_eq!(private_key.0.len(), 32); - } - - #[test] - fn test_file_encryptor_round_trip() { - let encryptor = XChaCha20FileEncryptor; - let key_deriver = Ed25519KeyDeriver; - - let seed = Seed::new(vec![1; 32]); - let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap(); - let did = Did::new(&public_key); - let password = "test-password"; - - // Encrypt - let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "univ:test:crypto", &[]).unwrap(); - - // Verify file structure - assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); - assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305"); - assert_eq!(encrypted_file.salt.len(), 32); - assert_eq!(encrypted_file.nonce.len(), 24); - assert_eq!(encrypted_file.public_key, public_key.0); - assert_eq!(encrypted_file.did, did.0); - - // Decrypt - let (decrypted_seed, decrypted_public_key, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap(); - - // Verify decryption - assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); - assert_eq!(decrypted_public_key.0, public_key.0); - } - - #[test] - fn test_file_encryptor_wrong_password_fails() { - let encryptor = XChaCha20FileEncryptor; - - let seed = Seed::new(vec![1, 2, 3, 4, 5]); - let public_key = PublicKey(vec![1; 32]); - let did = Did::new(&public_key); - - // Encrypt with one password - let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "univ:test:crypto", &[]).unwrap(); - - // Try to decrypt with wrong password - let result = encryptor.decrypt(&encrypted_file, "wrong-password"); - - // Should fail - assert!(result.is_err()); - } - - #[test] - fn test_file_encryptor_invalid_file_format() { - let encryptor = XChaCha20FileEncryptor; - - let mut invalid_file = PassportFile { - enc_seed: vec![1, 2, 3], - kdf: "Invalid-KDF".to_string(), - cipher: "Invalid-Cipher".to_string(), - salt: vec![0; 32], - nonce: vec![0; 24], - public_key: vec![1; 32], - did: "test-did".to_string(), - univ_id: "univ:test:crypto".to_string(), - created_at: 0, - version: "1.0.0".to_string(), - enc_user_profiles: vec![], - }; - - // Test with invalid KDF - let result = encryptor.decrypt(&invalid_file, "password"); - assert!(result.is_err()); - - // Test with invalid cipher - invalid_file.kdf = "HKDF-SHA256".to_string(); - invalid_file.cipher = "Invalid-Cipher".to_string(); - let result = encryptor.decrypt(&invalid_file, "password"); - assert!(result.is_err()); - } - - #[test] - fn test_file_encryptor_with_user_profiles() { - let encryptor = XChaCha20FileEncryptor; - let key_deriver = Ed25519KeyDeriver; - - let seed = Seed::new(vec![1; 32]); - let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap(); - let did = Did::new(&public_key); - let password = "test-password"; - - // Create test user profiles - let user_profiles = vec![ - UserProfile { - id: "test-uuid-default".to_string(), - hub_did: None, - identity: UserIdentity { - handle: Some("defaultuser".to_string()), - display_name: Some("Default User".to_string()), - first_name: Some("Default".to_string()), - last_name: Some("User".to_string()), - email: Some("default@example.com".to_string()), - avatar_url: None, - bio: Some("Default bio".to_string()), - }, - preferences: UserPreferences { - theme: Some("dark".to_string()), - language: Some("en".to_string()), - notifications_enabled: true, - auto_sync: true, - }, - created_at: 1234567890, - updated_at: 1234567890, - }, - UserProfile { - id: "test-uuid-hub".to_string(), - hub_did: Some("h:hub1".to_string()), - identity: UserIdentity { - handle: Some("hubuser".to_string()), - display_name: Some("Hub User".to_string()), - first_name: Some("Hub".to_string()), - last_name: Some("User".to_string()), - email: Some("hub@example.com".to_string()), - avatar_url: Some("https://example.com/avatar.png".to_string()), - bio: Some("Hub bio".to_string()), - }, - preferences: UserPreferences { - theme: Some("light".to_string()), - language: Some("fr".to_string()), - notifications_enabled: false, - auto_sync: false, - }, - created_at: 1234567891, - updated_at: 1234567892, - }, - ]; - - // Encrypt - let encrypted_file = encryptor.encrypt( - &seed, - password, - &public_key, - &did, - "univ:test:crypto", - &user_profiles - ).unwrap(); - - // Verify file structure includes user profiles - assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); - assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305"); - assert!(!encrypted_file.enc_user_profiles.is_empty()); - - // Decrypt - let (decrypted_seed, decrypted_public_key, _, decrypted_profiles) = - encryptor.decrypt(&encrypted_file, password).unwrap(); - - // Verify decryption - assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); - assert_eq!(decrypted_public_key.0, public_key.0); - - // Verify user profiles - assert_eq!(decrypted_profiles.len(), 2); - - // Verify default profile - let default_profile = decrypted_profiles.iter().find(|p| p.is_default()).unwrap(); - assert_eq!(default_profile.identity.handle, Some("defaultuser".to_string())); - assert_eq!(default_profile.identity.display_name, Some("Default User".to_string())); - assert_eq!(default_profile.identity.email, Some("default@example.com".to_string())); - assert_eq!(default_profile.preferences.theme, Some("dark".to_string())); - - // Verify hub profile - let hub_profile = decrypted_profiles.iter().find(|p| p.hub_did == Some("h:hub1".to_string())).unwrap(); - assert_eq!(hub_profile.identity.handle, Some("hubuser".to_string())); - assert_eq!(hub_profile.identity.display_name, Some("Hub User".to_string())); - assert_eq!(hub_profile.identity.email, Some("hub@example.com".to_string())); - assert_eq!(hub_profile.preferences.language, Some("fr".to_string())); - assert!(!hub_profile.preferences.notifications_enabled); - } - - #[test] - fn test_file_encryptor_with_empty_user_profiles() { - let encryptor = XChaCha20FileEncryptor; - let key_deriver = Ed25519KeyDeriver; - - let seed = Seed::new(vec![1; 32]); - let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap(); - let did = Did::new(&public_key); - let password = "test-password"; - - // Encrypt with empty user profiles - let encrypted_file = encryptor.encrypt( - &seed, - password, - &public_key, - &did, - "univ:test:crypto", - &[] - ).unwrap(); - - // Decrypt - let (_, _, _, decrypted_profiles) = encryptor.decrypt(&encrypted_file, password).unwrap(); - - // Should have empty profiles - assert_eq!(decrypted_profiles.len(), 0); - } -} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/mod.rs b/libs/sharenet-passport/src/infrastructure/mod.rs index 6a430c5..0bdf396 100644 --- a/libs/sharenet-passport/src/infrastructure/mod.rs +++ b/libs/sharenet-passport/src/infrastructure/mod.rs @@ -1,15 +1,14 @@ -#[cfg(feature = "std")] +// Core abstractions for all platforms +pub mod traits; +pub mod rng; +pub mod time; pub mod crypto; -#[cfg(feature = "std")] pub mod storage; -#[cfg(feature = "std")] -pub use crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor}; -#[cfg(feature = "std")] -pub use storage::FileSystemStorage; +// Export platform-appropriate implementations +pub use crypto::*; +pub use storage::*; -#[cfg(all(test, feature = "std"))] -mod crypto_test; +// Re-export traits for convenience +pub use traits::*; -#[cfg(all(test, feature = "std"))] -mod storage_test; \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/rng.rs b/libs/sharenet-passport/src/infrastructure/rng.rs new file mode 100644 index 0000000..3adf101 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/rng.rs @@ -0,0 +1,49 @@ +//! Random number generation abstraction for WASM compatibility + +use crate::domain::error::DomainError; + +/// Random number generator trait +pub trait RngCore { + /// Fill a buffer with random bytes + fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), DomainError>; +} + +/// Standard library RNG using OsRng +#[cfg(not(target_arch = "wasm32"))] +pub struct StdRng; + +#[cfg(not(target_arch = "wasm32"))] +impl RngCore for StdRng { + fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), DomainError> { + use rand::{rngs::OsRng, RngCore}; + + OsRng + .try_fill_bytes(dest) + .map_err(|e| DomainError::CryptographicError(format!("RNG error: {}", e))) + } +} + +/// WASM-compatible RNG using getrandom +#[cfg(target_arch = "wasm32")] +pub struct WasmRng; + +#[cfg(target_arch = "wasm32")] +impl RngCore for WasmRng { + fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), DomainError> { + getrandom::getrandom(dest) + .map_err(|e| DomainError::CryptographicError(format!("WASM RNG error: {}", e))) + } +} + +/// Create a new RNG instance based on the current architecture +pub fn new_rng() -> Box { + #[cfg(not(target_arch = "wasm32"))] + { + Box::new(StdRng) + } + + #[cfg(target_arch = "wasm32")] + { + Box::new(WasmRng) + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/storage/mod.rs b/libs/sharenet-passport/src/infrastructure/storage/mod.rs new file mode 100644 index 0000000..0ce0a43 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/storage/mod.rs @@ -0,0 +1,22 @@ +//! Unified storage API with target-specific implementations + +// Platform-specific implementations +#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))] +mod native; + +#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))] +mod wasm; + +// Re-export the unified API +#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))] +pub use native::FileSystemStorage; + +#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))] +pub use wasm::BrowserStorage as FileSystemStorage; + +// Target-specific tests +#[cfg(all(test, all(not(target_arch = "wasm32"), not(feature = "force-wasm"))))] +mod native_test; + +#[cfg(all(test, any(target_arch = "wasm32", feature = "force-wasm")))] +mod wasm_test; \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/storage.rs b/libs/sharenet-passport/src/infrastructure/storage/native.rs similarity index 92% rename from libs/sharenet-passport/src/infrastructure/storage.rs rename to libs/sharenet-passport/src/infrastructure/storage/native.rs index 84795fa..5ce0f6d 100644 --- a/libs/sharenet-passport/src/infrastructure/storage.rs +++ b/libs/sharenet-passport/src/infrastructure/storage/native.rs @@ -1,10 +1,13 @@ +//! Native (std) file system storage implementation + use std::fs; use std::path::Path; -use crate::domain::entities::PassportFile; -use crate::domain::traits::FileStorage; +use crate::domain::entities::*; use crate::domain::error::DomainError; +use crate::domain::traits::*; +/// Native file system storage #[derive(Clone)] pub struct FileSystemStorage; diff --git a/libs/sharenet-passport/src/infrastructure/storage/native_test.rs b/libs/sharenet-passport/src/infrastructure/storage/native_test.rs new file mode 100644 index 0000000..4b4168a --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/storage/native_test.rs @@ -0,0 +1,58 @@ +//! Native-specific storage tests + +#[cfg(test)] +mod tests { + use crate::domain::entities::*; + use crate::domain::traits::FileStorage; + use crate::infrastructure::storage::FileSystemStorage; + + #[test] + fn test_native_storage_save_and_load() { + let storage = FileSystemStorage; + let temp_dir = tempfile::tempdir().unwrap(); + let file_path = temp_dir.path().join("test-passport.spf"); + + // Create a test passport file + let test_file = PassportFile { + kdf: "HKDF-SHA256".to_string(), + cipher: "XChaCha20-Poly1305".to_string(), + salt: vec![1; 32], + nonce: vec![2; 24], + enc_seed: vec![3; 32], + public_key: vec![4; 32], + did: "did:sharenet:test".to_string(), + univ_id: "u:Test Universe:12345678-1234-1234-1234-123456789012".to_string(), + created_at: 1234567890, + version: "1.0.0".to_string(), + enc_user_profiles: vec![], + }; + + // Save the file + storage.save(&test_file, file_path.to_str().unwrap()).unwrap(); + + // Load the file + let loaded_file = storage.load(file_path.to_str().unwrap()).unwrap(); + + // Verify the loaded file matches the original + assert_eq!(loaded_file.kdf, test_file.kdf); + assert_eq!(loaded_file.cipher, test_file.cipher); + assert_eq!(loaded_file.salt, test_file.salt); + assert_eq!(loaded_file.nonce, test_file.nonce); + assert_eq!(loaded_file.enc_seed, test_file.enc_seed); + assert_eq!(loaded_file.public_key, test_file.public_key); + assert_eq!(loaded_file.did, test_file.did); + assert_eq!(loaded_file.univ_id, test_file.univ_id); + assert_eq!(loaded_file.created_at, test_file.created_at); + assert_eq!(loaded_file.version, test_file.version); + assert_eq!(loaded_file.enc_user_profiles, test_file.enc_user_profiles); + } + + #[test] + fn test_native_storage_load_nonexistent_file_fails() { + let storage = FileSystemStorage; + let result = storage.load("/nonexistent/path/file.spf"); + + // Should fail + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/storage/wasm.rs b/libs/sharenet-passport/src/infrastructure/storage/wasm.rs new file mode 100644 index 0000000..bd2a840 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/storage/wasm.rs @@ -0,0 +1,110 @@ +//! WASM-compatible storage using browser LocalStorage + +use crate::domain::entities::*; +use crate::domain::error::DomainError; +use crate::domain::traits::*; + +/// WASM storage using browser LocalStorage +#[derive(Clone)] +pub struct BrowserStorage; + +// Mock storage for testing on native targets +#[cfg(not(target_arch = "wasm32"))] +use std::collections::HashMap; +#[cfg(not(target_arch = "wasm32"))] +use std::sync::{Mutex, OnceLock}; + +#[cfg(not(target_arch = "wasm32"))] +static MOCK_STORAGE: OnceLock>> = OnceLock::new(); + +impl FileStorage for BrowserStorage { + type Error = DomainError; + + fn save(&self, file: &PassportFile, path: &str) -> Result<(), Self::Error> { + // Real implementation for WASM targets + #[cfg(target_arch = "wasm32")] + { + use base64::Engine; + use gloo_storage::Storage; + + // Serialize to CBOR + let data = serde_cbor::to_vec(file) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to serialize file: {}", e)))?; + + // Convert to base64 for storage + let base64_data = base64::engine::general_purpose::STANDARD.encode(&data); + + // Store in browser localStorage using gloo-storage (synchronous) + gloo_storage::LocalStorage::set(path, base64_data) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to store in localStorage: {}", e)))?; + + Ok(()) + } + + // Mock implementation for testing on native targets + #[cfg(not(target_arch = "wasm32"))] + { + // For testing purposes, we'll use a simple in-memory storage + // In a real browser environment, this would use actual localStorage + use base64::Engine; + + // Serialize to CBOR + let data = serde_cbor::to_vec(file) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to serialize file: {}", e)))?; + + // Convert to base64 for storage + let base64_data = base64::engine::general_purpose::STANDARD.encode(&data); + + // Store in mock storage + let mock_storage = MOCK_STORAGE.get_or_init(|| Mutex::new(HashMap::new())); + mock_storage.lock().unwrap().insert(path.to_string(), base64_data); + + Ok(()) + } + } + + fn load(&self, path: &str) -> Result { + // Real implementation for WASM targets + #[cfg(target_arch = "wasm32")] + { + use base64::Engine; + use gloo_storage::Storage; + + // Load from browser localStorage (synchronous) + let base64_data: String = gloo_storage::LocalStorage::get(path) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to load from localStorage: {}", e)))?; + + // Decode from base64 + let data = base64::engine::general_purpose::STANDARD.decode(&base64_data) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to decode base64: {}", e)))?; + + // Deserialize from CBOR + let file = serde_cbor::from_slice(&data) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to deserialize file: {}", e)))?; + + Ok(file) + } + + // Mock implementation for testing on native targets + #[cfg(not(target_arch = "wasm32"))] + { + use base64::Engine; + + // Load from mock storage + let mock_storage = MOCK_STORAGE.get_or_init(|| Mutex::new(HashMap::new())); + let mock_storage = mock_storage.lock().unwrap(); + let base64_data = mock_storage.get(path) + .ok_or_else(|| DomainError::InvalidFileFormat(format!("Key not found: {}", path)))?; + + // Decode from base64 + let data = base64::engine::general_purpose::STANDARD.decode(base64_data) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to decode base64: {}", e)))?; + + // Deserialize from CBOR + let file = serde_cbor::from_slice(&data) + .map_err(|e| DomainError::InvalidFileFormat(format!("Failed to deserialize file: {}", e)))?; + + Ok(file) + } + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/storage/wasm_test.rs b/libs/sharenet-passport/src/infrastructure/storage/wasm_test.rs new file mode 100644 index 0000000..ab32771 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/storage/wasm_test.rs @@ -0,0 +1,57 @@ +//! WASM-specific storage tests + +#[cfg(test)] +mod tests { + use crate::domain::entities::*; + use crate::domain::traits::FileStorage; + use crate::infrastructure::storage::FileSystemStorage; + + #[test] + fn test_wasm_storage_save_and_load() { + let storage = FileSystemStorage; + let test_key = "test-passport-key"; + + // Create a test passport file + let test_file = PassportFile { + kdf: "HKDF-SHA256".to_string(), + cipher: "XChaCha20-Poly1305".to_string(), + salt: vec![1; 32], + nonce: vec![2; 24], + enc_seed: vec![3; 32], + public_key: vec![4; 32], + did: "did:sharenet:test".to_string(), + univ_id: "u:Test Universe:12345678-1234-1234-1234-123456789012".to_string(), + created_at: 1234567890, + version: "1.0.0".to_string(), + enc_user_profiles: vec![], + }; + + // Save the file + storage.save(&test_file, test_key).unwrap(); + + // Load the file + let loaded_file = storage.load(test_key).unwrap(); + + // Verify the loaded file matches the original + assert_eq!(loaded_file.kdf, test_file.kdf); + assert_eq!(loaded_file.cipher, test_file.cipher); + assert_eq!(loaded_file.salt, test_file.salt); + assert_eq!(loaded_file.nonce, test_file.nonce); + assert_eq!(loaded_file.enc_seed, test_file.enc_seed); + assert_eq!(loaded_file.public_key, test_file.public_key); + assert_eq!(loaded_file.did, test_file.did); + assert_eq!(loaded_file.univ_id, test_file.univ_id); + assert_eq!(loaded_file.created_at, test_file.created_at); + assert_eq!(loaded_file.version, test_file.version); + assert_eq!(loaded_file.enc_user_profiles, test_file.enc_user_profiles); + } + + #[test] + fn test_wasm_storage_load_nonexistent_key_fails() { + let storage = FileSystemStorage; + let result = storage.load("nonexistent-key"); + + // Should fail + assert!(result.is_err()); + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/storage_test.rs b/libs/sharenet-passport/src/infrastructure/storage_test.rs deleted file mode 100644 index 916be53..0000000 --- a/libs/sharenet-passport/src/infrastructure/storage_test.rs +++ /dev/null @@ -1,65 +0,0 @@ -#[cfg(test)] -mod tests { - use tempfile::NamedTempFile; - use crate::domain::traits::FileStorage; - use crate::{FileSystemStorage, PassportFile}; - - #[test] - fn test_file_storage_round_trip() { - let storage = FileSystemStorage; - let temp_file = NamedTempFile::new().unwrap(); - let file_path = temp_file.path().to_str().unwrap(); - - let test_file = PassportFile { - enc_seed: vec![1, 2, 3, 4, 5], - kdf: "HKDF-SHA256".to_string(), - cipher: "XChaCha20-Poly1305".to_string(), - salt: vec![0; 32], - nonce: vec![0; 24], - public_key: vec![1; 32], - did: "test-did".to_string(), - univ_id: "univ:test:storage".to_string(), - created_at: 1234567890, - version: "1.0.0".to_string(), - enc_user_profiles: vec![], - }; - - // Save file - storage.save(&test_file, file_path).unwrap(); - - // Load file - let loaded_file = storage.load(file_path).unwrap(); - - // Verify data integrity - assert_eq!(loaded_file.enc_seed, test_file.enc_seed); - assert_eq!(loaded_file.kdf, test_file.kdf); - assert_eq!(loaded_file.cipher, test_file.cipher); - assert_eq!(loaded_file.salt, test_file.salt); - assert_eq!(loaded_file.nonce, test_file.nonce); - assert_eq!(loaded_file.public_key, test_file.public_key); - assert_eq!(loaded_file.did, test_file.did); - assert_eq!(loaded_file.created_at, test_file.created_at); - assert_eq!(loaded_file.version, test_file.version); - } - - #[test] - fn test_file_storage_nonexistent_file() { - let storage = FileSystemStorage; - let result = storage.load("/nonexistent/path/file.spf"); - - assert!(result.is_err()); - } - - #[test] - fn test_file_storage_invalid_cbor() { - let storage = FileSystemStorage; - let temp_file = NamedTempFile::new().unwrap(); - let file_path = temp_file.path().to_str().unwrap(); - - // Write invalid CBOR data - std::fs::write(file_path, b"invalid cbor data").unwrap(); - - let result = storage.load(file_path); - assert!(result.is_err()); - } -} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/time.rs b/libs/sharenet-passport/src/infrastructure/time.rs new file mode 100644 index 0000000..4e78033 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/time.rs @@ -0,0 +1,52 @@ +//! Time abstraction for WASM compatibility + +use crate::domain::error::DomainError; + +/// Time provider trait for abstracting time operations +pub trait TimeProvider { + /// Get current timestamp in seconds since Unix epoch + fn now_seconds() -> Result; +} + +/// Standard library time provider +#[cfg(not(target_arch = "wasm32"))] +pub struct StdTimeProvider; + +#[cfg(not(target_arch = "wasm32"))] +impl TimeProvider for StdTimeProvider { + fn now_seconds() -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| DomainError::CryptographicError(format!("Time error: {}", e))) + .map(|d| d.as_secs()) + } +} + +/// WASM time provider +#[cfg(target_arch = "wasm32")] +pub struct WasmTimeProvider; + +#[cfg(target_arch = "wasm32")] +impl TimeProvider for WasmTimeProvider { + fn now_seconds() -> Result { + // Use JavaScript Date API via js_sys + // This will work when compiled to WASM + let timestamp = js_sys::Date::now() / 1000.0; // Convert from milliseconds to seconds + Ok(timestamp as u64) + } +} + +/// Get the current timestamp using the appropriate time provider +pub fn now_seconds() -> Result { + #[cfg(not(target_arch = "wasm32"))] + { + StdTimeProvider::now_seconds() + } + + #[cfg(target_arch = "wasm32")] + { + WasmTimeProvider::now_seconds() + } +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/traits.rs b/libs/sharenet-passport/src/infrastructure/traits.rs new file mode 100644 index 0000000..8811849 --- /dev/null +++ b/libs/sharenet-passport/src/infrastructure/traits.rs @@ -0,0 +1,65 @@ +//! Core abstractions for platform-agnostic cryptography and storage + +use crate::domain::entities::*; +use crate::domain::error::DomainError; + +/// Mnemonic generation trait +pub trait MnemonicGenerator { + type Error: Into; + + fn generate(&self) -> Result; + fn validate(&self, words: &[String]) -> Result<(), Self::Error>; +} + +/// Key derivation trait +pub trait KeyDeriver { + type Error: Into; + + fn derive_from_seed(&self, seed: &Seed) -> Result<(PublicKey, PrivateKey), Self::Error>; + fn derive_from_mnemonic(&self, mnemonic: &RecoveryPhrase, univ_id: &str) -> Result; +} + +/// File encryption trait +pub trait FileEncryptor { + type Error: Into; + + fn encrypt( + &self, + seed: &Seed, + password: &str, + public_key: &PublicKey, + did: &Did, + univ_id: &str, + user_profiles: &[UserProfile], + ) -> Result; + + fn decrypt( + &self, + file: &PassportFile, + password: &str, + ) -> Result<(Seed, PublicKey, PrivateKey, Vec), Self::Error>; +} + +/// Storage trait for passport files +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait(?Send))] +pub trait FileStorage { + type Error: Into; + + async fn save(&self, file: &PassportFile, path: &str) -> Result<(), Self::Error>; + async fn load(&self, path: &str) -> Result; +} + +/// Random number generation trait +pub trait RngCore { + type Error: Into; + + fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error>; +} + +/// Time provider trait +pub trait TimeProvider { + type Error: Into; + + fn now_seconds(&self) -> Result; +} \ No newline at end of file diff --git a/libs/sharenet-passport/src/lib.rs b/libs/sharenet-passport/src/lib.rs index d5b58a7..6e80e98 100644 --- a/libs/sharenet-passport/src/lib.rs +++ b/libs/sharenet-passport/src/lib.rs @@ -21,9 +21,10 @@ pub use application::use_cases::{ }; pub use application::error::ApplicationError; +// Re-export infrastructure implementations (automatically selected by target) pub use infrastructure::{ Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, - FileSystemStorage + FileSystemStorage, }; \ No newline at end of file diff --git a/sharenet-passport-cli/src/bin/test_universe_binding.rs b/sharenet-passport-cli/src/bin/test_universe_binding.rs index 352dfcc..3f3fc62 100644 --- a/sharenet-passport-cli/src/bin/test_universe_binding.rs +++ b/sharenet-passport-cli/src/bin/test_universe_binding.rs @@ -1,7 +1,8 @@ -use sharenet_passport::application::use_cases::*; -use sharenet_passport::domain::traits::*; -use sharenet_passport::infrastructure::crypto::*; -use sharenet_passport::infrastructure::storage::*; +use sharenet_passport::{ + application::use_cases::*, + Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, FileSystemStorage, + FileStorage, +}; use std::fs; fn main() { diff --git a/sharenet-passport-cli/src/cli/interface.rs b/sharenet-passport-cli/src/cli/interface.rs index 2722dc1..0fd3753 100644 --- a/sharenet-passport-cli/src/cli/interface.rs +++ b/sharenet-passport-cli/src/cli/interface.rs @@ -1,9 +1,8 @@ use sharenet_passport::{ application::use_cases::*, domain::entities::{UserIdentity, UserPreferences}, - infrastructure::*, - ApplicationError, - FileStorage, + Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, FileSystemStorage, + ApplicationError, FileStorage, }; use rpassword::prompt_password; use hex;