From 3c9320ffa5f633f81d1a09f01eeab8f20e3b26f9 Mon Sep 17 00:00:00 2001 From: JohnE Date: Mon, 23 Feb 2026 14:59:04 -0800 Subject: [PATCH] NEW: first draft of crates --- .cargo/config.toml | 29 + .gitignore | 21 + .gitmodules | 3 + .vscode/settings.json | 7 +- Cargo.lock | 1063 +++++++++++++++++ Cargo.toml | 29 + crates/ccc-crypto-core/Cargo.toml | 18 + crates/ccc-crypto-core/src/algorithms.rs | 295 +++++ crates/ccc-crypto-core/src/capabilities.rs | 135 +++ crates/ccc-crypto-core/src/error.rs | 46 + crates/ccc-crypto-core/src/lib.rs | 24 + crates/ccc-crypto-core/src/provider.rs | 197 +++ crates/ccc-crypto-core/src/registry.rs | 237 ++++ crates/ccc-crypto-core/src/types.rs | 98 ++ crates/ccc-crypto-wolfssl/Cargo.toml | 28 + crates/ccc-crypto-wolfssl/build.rs | 204 ++++ crates/ccc-crypto-wolfssl/src/aead.rs | 303 +++++ crates/ccc-crypto-wolfssl/src/capabilities.rs | 200 ++++ crates/ccc-crypto-wolfssl/src/hash.rs | 135 +++ crates/ccc-crypto-wolfssl/src/kdf.rs | 167 +++ crates/ccc-crypto-wolfssl/src/kem.rs | 313 +++++ crates/ccc-crypto-wolfssl/src/lib.rs | 206 ++++ crates/ccc-crypto-wolfssl/src/mac.rs | 173 +++ crates/ccc-crypto-wolfssl/src/provider.rs | 250 ++++ crates/ccc-flutter-bridge/Cargo.toml | 28 + crates/ccc-flutter-bridge/src/bridge.rs | 279 +++++ crates/ccc-flutter-bridge/src/dto.rs | 296 +++++ crates/ccc-flutter-bridge/src/lib.rs | 19 + docs/ccc_rust_plan.rst | 448 +++++++ docs/ccc_rust_plan_phases.rst | 392 ++++++ flutter_rust_bridge.yaml | 23 + rust-toolchain.toml | 15 + tests/conformance/Cargo.toml | 19 + tests/conformance/src/main.rs | 332 +++++ vendors/README.md | 56 + vendors/wolfssl | 1 + 36 files changed, 6088 insertions(+), 1 deletion(-) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/ccc-crypto-core/Cargo.toml create mode 100644 crates/ccc-crypto-core/src/algorithms.rs create mode 100644 crates/ccc-crypto-core/src/capabilities.rs create mode 100644 crates/ccc-crypto-core/src/error.rs create mode 100644 crates/ccc-crypto-core/src/lib.rs create mode 100644 crates/ccc-crypto-core/src/provider.rs create mode 100644 crates/ccc-crypto-core/src/registry.rs create mode 100644 crates/ccc-crypto-core/src/types.rs create mode 100644 crates/ccc-crypto-wolfssl/Cargo.toml create mode 100644 crates/ccc-crypto-wolfssl/build.rs create mode 100644 crates/ccc-crypto-wolfssl/src/aead.rs create mode 100644 crates/ccc-crypto-wolfssl/src/capabilities.rs create mode 100644 crates/ccc-crypto-wolfssl/src/hash.rs create mode 100644 crates/ccc-crypto-wolfssl/src/kdf.rs create mode 100644 crates/ccc-crypto-wolfssl/src/kem.rs create mode 100644 crates/ccc-crypto-wolfssl/src/lib.rs create mode 100644 crates/ccc-crypto-wolfssl/src/mac.rs create mode 100644 crates/ccc-crypto-wolfssl/src/provider.rs create mode 100644 crates/ccc-flutter-bridge/Cargo.toml create mode 100644 crates/ccc-flutter-bridge/src/bridge.rs create mode 100644 crates/ccc-flutter-bridge/src/dto.rs create mode 100644 crates/ccc-flutter-bridge/src/lib.rs create mode 100644 docs/ccc_rust_plan.rst create mode 100644 docs/ccc_rust_plan_phases.rst create mode 100644 flutter_rust_bridge.yaml create mode 100644 rust-toolchain.toml create mode 100644 tests/conformance/Cargo.toml create mode 100644 tests/conformance/src/main.rs create mode 100644 vendors/README.md create mode 160000 vendors/wolfssl diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..77df2d5 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,29 @@ +# Cross-compilation target aliases +# Usage: cargo build-ios / cargo build-android-arm64 etc. +[alias] +build-ios = "build --target aarch64-apple-ios" +build-ios-sim = "build --target aarch64-apple-ios-sim" +build-android-arm64 = "build --target aarch64-linux-android" +build-android-x64 = "build --target x86_64-linux-android" +build-macos-arm64 = "build --target aarch64-apple-darwin" +build-macos-x64 = "build --target x86_64-apple-darwin" +build-all-apple = "build --target aarch64-apple-ios --target aarch64-apple-darwin --target x86_64-apple-darwin" + +# Link flags for Apple targets (needed when linking wolfSSL static lib) +[target.aarch64-apple-ios] +rustflags = ["-C", "link-arg=-lc++"] + +[target.aarch64-apple-darwin] +rustflags = ["-C", "link-arg=-lc++"] + +[target.x86_64-apple-darwin] +rustflags = ["-C", "link-arg=-lc++"] + +# Android NDK sysroot — set ANDROID_NDK_HOME in your environment. +# cargokit (used by flutter_rust_bridge) handles this automatically during +# `flutter build apk` / `flutter build appbundle`. +[target.aarch64-linux-android] +linker = "aarch64-linux-android21-clang" + +[target.x86_64-linux-android] +linker = "x86_64-linux-android21-clang" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad67955 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bf82fe2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendors/wolfssl"] + path = vendors/wolfssl + url = https://github.com/wolfSSL/wolfssl diff --git a/.vscode/settings.json b/.vscode/settings.json index abde59f..71111c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,10 @@ "**/var/**": true, "**/bin/**": true }, - "foldOnOpen.targets": ["AllBlockComments"] + "foldOnOpen.targets": [ + "AllBlockComments" + ], + "chat.tools.terminal.autoApprove": { + "cargo search": true + } } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..45b91a7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1063 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" +dependencies = [ + "anyhow", + "atomic", + "backtrace", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger 0.10.2", + "log", + "once_cell", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "ccc-conformance-tests" +version = "0.1.0" +dependencies = [ + "anyhow", + "ccc-crypto-core", + "ccc-crypto-wolfssl", + "env_logger 0.11.9", + "hex", + "log", +] + +[[package]] +name = "ccc-crypto-core" +version = "0.1.0" +dependencies = [ + "log", + "once_cell", + "serde", + "serde_json", + "thiserror", + "zeroize", +] + +[[package]] +name = "ccc-crypto-wolfssl" +version = "0.1.0" +dependencies = [ + "bindgen", + "ccc-crypto-core", + "cmake", + "hex", + "log", + "thiserror", + "zeroize", +] + +[[package]] +name = "ccc-flutter-bridge" +version = "0.1.0" +dependencies = [ + "anyhow", + "ccc-crypto-core", + "ccc-crypto-wolfssl", + "env_logger 0.11.9", + "flutter_rust_bridge", + "hex", + "log", + "zeroize", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", +] + +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flutter_rust_bridge" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8c0dee6249225e815dcff3f3a39b98d9f66fdb3c392a432715b646bfa4da02" +dependencies = [ + "allo-isolate", + "android_logger", + "anyhow", + "build-target", + "bytemuck", + "byteorder", + "console_error_panic_hook", + "dart-sys", + "delegate-attr", + "flutter_rust_bridge_macros", + "futures", + "js-sys", + "lazy_static", + "log", + "oslog", + "portable-atomic", + "threadpool", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88d604908d9eccb4ca9c26640ce41033165cbef041460e704ae28bd5208bce" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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 = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oslog" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8343ce955f18e7e68c0207dd0ea776ec453035685395ababd2ea651c569728b3" +dependencies = [ + "cc", + "dashmap", + "log", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cbb0770 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[workspace] +resolver = "2" +members = [ + "crates/ccc-crypto-core", + "crates/ccc-crypto-wolfssl", + "crates/ccc-flutter-bridge", + "tests/conformance", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +authors = ["LetUsMsg Engineering"] +license = "MIT" +repository = "https://github.com/letusmsg/ccc_rust" + +# Shared dependency versions — all crates inherit from here. +[workspace.dependencies] +zeroize = { version = "1.8", features = ["derive"] } +thiserror = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" +once_cell = "1.19" +anyhow = "1.0" +env_logger = "0.11" + +# ccc-crypto-core is referenced by every crate in the workspace. +ccc-crypto-core = { path = "crates/ccc-crypto-core" } diff --git a/crates/ccc-crypto-core/Cargo.toml b/crates/ccc-crypto-core/Cargo.toml new file mode 100644 index 0000000..f1b8ce3 --- /dev/null +++ b/crates/ccc-crypto-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ccc-crypto-core" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "Shared traits, algorithm enumerations, and provider registry for the CCC crypto system." + +[dependencies] +zeroize.workspace = true +thiserror.workspace = true +serde.workspace = true +serde_json.workspace = true +log.workspace = true +once_cell.workspace = true + +[dev-dependencies] +# Nothing external needed for registry unit tests. diff --git a/crates/ccc-crypto-core/src/algorithms.rs b/crates/ccc-crypto-core/src/algorithms.rs new file mode 100644 index 0000000..b75f5b5 --- /dev/null +++ b/crates/ccc-crypto-core/src/algorithms.rs @@ -0,0 +1,295 @@ +//! Algorithm enumerations for the CCC crypto system. +//! +//! **Important**: every discriminant value matches the corresponding integer +//! constant in `cipher_constants.dart`. Do NOT change them without making a +//! matching change in Dart. + +use serde::{Deserialize, Serialize}; + +// ────────────────────────────────────────────────────────────────────────────── +// AEAD (Authenticated Encryption with Associated Data) +// Dart constants 10–19 +// ────────────────────────────────────────────────────────────────────────────── + +/// Authenticated encryption algorithms supported by the CCC system. +/// +/// Discriminant values match `cipher_constants.dart` (e.g. `AES_GCM_256 = 12`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u32)] +pub enum AeadAlgorithm { + /// AES-256-GCM. Dart constant `12`. + AesGcm256 = 12, + /// ChaCha20-Poly1305 (12-byte nonce). Dart constant `13`. + ChaCha20Poly1305 = 13, + /// XChaCha20-Poly1305 (24-byte nonce). Dart constant `14`. + XChaCha20Poly1305 = 14, + /// Ascon-AEAD-128a (lightweight AEAD). Dart constant `15`. + Ascon128a = 15, +} + +impl AeadAlgorithm { + /// Convert from the raw `u32` value used in FFI / Dart interop. + pub fn from_u32(v: u32) -> Option { + match v { + 12 => Some(Self::AesGcm256), + 13 => Some(Self::ChaCha20Poly1305), + 14 => Some(Self::XChaCha20Poly1305), + 15 => Some(Self::Ascon128a), + _ => None, + } + } + + /// Human-readable name, suitable for logs and diagnostics. + pub fn name(self) -> &'static str { + match self { + Self::AesGcm256 => "AES-256-GCM", + Self::ChaCha20Poly1305 => "ChaCha20-Poly1305", + Self::XChaCha20Poly1305 => "XChaCha20-Poly1305", + Self::Ascon128a => "Ascon-AEAD-128a", + } + } + + /// Expected nonce length in bytes for this algorithm. + pub fn nonce_len(self) -> usize { + match self { + Self::AesGcm256 => 12, + Self::ChaCha20Poly1305 => 12, + Self::XChaCha20Poly1305 => 24, + Self::Ascon128a => 16, + } + } + + /// Expected key length in bytes for this algorithm. + pub fn key_len(self) -> usize { + match self { + Self::AesGcm256 => 32, + Self::ChaCha20Poly1305 => 32, + Self::XChaCha20Poly1305 => 32, + Self::Ascon128a => 16, + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// KDF (Key Derivation Functions) +// Dart constants 1–4 +// ────────────────────────────────────────────────────────────────────────────── + +/// Key derivation function algorithms. +/// +/// Discriminant values match `cipher_constants.dart` (e.g. `SHA_256 = 1`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u32)] +pub enum KdfAlgorithm { + /// HKDF / chain-KDF using SHA-256. Dart constant `1`. + Sha256 = 1, + /// HKDF / chain-KDF using SHA-384. Dart constant `2`. + Sha384 = 2, + /// HKDF / chain-KDF using SHA-512. Dart constant `3`. + Sha512 = 3, + /// HKDF / chain-KDF using BLAKE2b-512. Dart constant `4`. + Blake2b512 = 4, + /// Argon2id (password / memory-hard KDF). Dart constant `5`. + Argon2id = 5, + /// KMAC256 (used by Ultra-mode seq_seed). Dart constant `6`. + Kmac256 = 6, +} + +impl KdfAlgorithm { + /// Convert from the raw `u32` value used in FFI / Dart interop. + pub fn from_u32(v: u32) -> Option { + match v { + 1 => Some(Self::Sha256), + 2 => Some(Self::Sha384), + 3 => Some(Self::Sha512), + 4 => Some(Self::Blake2b512), + 5 => Some(Self::Argon2id), + 6 => Some(Self::Kmac256), + _ => None, + } + } + + /// Human-readable name. + pub fn name(self) -> &'static str { + match self { + Self::Sha256 => "HKDF-SHA-256", + Self::Sha384 => "HKDF-SHA-384", + Self::Sha512 => "HKDF-SHA-512", + Self::Blake2b512 => "HKDF-BLAKE2b-512", + Self::Argon2id => "Argon2id", + Self::Kmac256 => "KMAC256", + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// MAC (Message Authentication Codes) +// Dart constants 30–35 +// ────────────────────────────────────────────────────────────────────────────── + +/// Message authentication code algorithms. +/// +/// Discriminant values match `cipher_constants.dart`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u32)] +pub enum MacAlgorithm { + /// HMAC-SHA-256. Dart constant `30`. + HmacSha256 = 30, + /// HMAC-SHA-384. Dart constant `31`. + HmacSha384 = 31, + /// HMAC-SHA-512. Dart constant `32`. + HmacSha512 = 32, + /// BLAKE2b-MAC (keyed BLAKE2b). Dart constant `33`. + Blake2bMac = 33, + /// Poly1305 standalone. Dart constant `35`. + Poly1305 = 35, +} + +impl MacAlgorithm { + /// Convert from the raw `u32` value. + pub fn from_u32(v: u32) -> Option { + match v { + 30 => Some(Self::HmacSha256), + 31 => Some(Self::HmacSha384), + 32 => Some(Self::HmacSha512), + 33 => Some(Self::Blake2bMac), + 35 => Some(Self::Poly1305), + _ => None, + } + } + + /// Human-readable name. + pub fn name(self) -> &'static str { + match self { + Self::HmacSha256 => "HMAC-SHA-256", + Self::HmacSha384 => "HMAC-SHA-384", + Self::HmacSha512 => "HMAC-SHA-512", + Self::Blake2bMac => "BLAKE2b-MAC", + Self::Poly1305 => "Poly1305", + } + } + + /// Output / tag length in bytes. + pub fn tag_len(self) -> usize { + match self { + Self::HmacSha256 => 32, + Self::HmacSha384 => 48, + Self::HmacSha512 => 64, + Self::Blake2bMac => 64, + Self::Poly1305 => 16, + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Hash +// Dart constants 40–49 +// ────────────────────────────────────────────────────────────────────────────── + +/// Cryptographic hash algorithms. +/// +/// Discriminant values match `cipher_constants.dart`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u32)] +pub enum HashAlgorithm { + /// SHA-256. Dart constant `40`. + Sha256 = 40, + /// SHA-384. Dart constant `41`. + Sha384 = 41, + /// SHA-512. Dart constant `42`. + Sha512 = 42, + /// BLAKE2b-512. Dart constant `43`. + Blake2b512 = 43, + /// SHA3-256. Dart constant `44`. + Sha3_256 = 44, + /// SHA3-512. Dart constant `45`. + Sha3_512 = 45, +} + +impl HashAlgorithm { + /// Convert from the raw `u32` value. + pub fn from_u32(v: u32) -> Option { + match v { + 40 => Some(Self::Sha256), + 41 => Some(Self::Sha384), + 42 => Some(Self::Sha512), + 43 => Some(Self::Blake2b512), + 44 => Some(Self::Sha3_256), + 45 => Some(Self::Sha3_512), + _ => None, + } + } + + /// Human-readable name. + pub fn name(self) -> &'static str { + match self { + Self::Sha256 => "SHA-256", + Self::Sha384 => "SHA-384", + Self::Sha512 => "SHA-512", + Self::Blake2b512 => "BLAKE2b-512", + Self::Sha3_256 => "SHA3-256", + Self::Sha3_512 => "SHA3-512", + } + } + + /// Digest output length in bytes. + pub fn digest_len(self) -> usize { + match self { + Self::Sha256 => 32, + Self::Sha384 => 48, + Self::Sha512 => 64, + Self::Blake2b512 => 64, + Self::Sha3_256 => 32, + Self::Sha3_512 => 64, + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// KEM (Key Encapsulation Mechanisms) +// Dart constants 50–59 (new range, not yet in cipher_constants.dart) +// ────────────────────────────────────────────────────────────────────────────── + +/// Key encapsulation mechanism algorithms. +/// +/// Values 50–59 are a new range reserved for KEM algorithms; add the +/// matching constants to `cipher_constants.dart` when wiring the Dart layer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(u32)] +pub enum KemAlgorithm { + /// X25519 ECDH. Dart constant `50`. + X25519 = 50, + /// X448 ECDH. Dart constant `51`. + X448 = 51, + /// ML-KEM-768 (FIPS 203). Dart constant `52`. + MlKem768 = 52, + /// ML-KEM-1024 (FIPS 203). Dart constant `53`. + MlKem1024 = 53, + /// Classic McEliece 460896. Dart constant `54`. + ClassicMcEliece460896 = 54, +} + +impl KemAlgorithm { + /// Convert from the raw `u32` value. + pub fn from_u32(v: u32) -> Option { + match v { + 50 => Some(Self::X25519), + 51 => Some(Self::X448), + 52 => Some(Self::MlKem768), + 53 => Some(Self::MlKem1024), + 54 => Some(Self::ClassicMcEliece460896), + _ => None, + } + } + + /// Human-readable name. + pub fn name(self) -> &'static str { + match self { + Self::X25519 => "X25519", + Self::X448 => "X448", + Self::MlKem768 => "ML-KEM-768", + Self::MlKem1024 => "ML-KEM-1024", + Self::ClassicMcEliece460896 => "Classic-McEliece-460896", + } + } +} diff --git a/crates/ccc-crypto-core/src/capabilities.rs b/crates/ccc-crypto-core/src/capabilities.rs new file mode 100644 index 0000000..4a8b4ae --- /dev/null +++ b/crates/ccc-crypto-core/src/capabilities.rs @@ -0,0 +1,135 @@ +//! Capability structs describing what each provider can do. +//! +//! [`ProviderCapabilities`] is populated by a provider at startup via a live +//! probe call (not hard-coded). The populated map is then exposed to Dart +//! via the `ccc_provider_capabilities()` bridge function, replacing the former +//! compile-time stub in `ccc_provider_spec.dart`. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}; + +// ────────────────────────────────────────────────────────────────────────────── + +/// Runtime capability descriptor for a single algorithm within a provider. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AlgorithmCapability { + /// Whether the algorithm is usable on this platform / build. + /// + /// Determined by a live probe call at startup, not by compile-time flags. + pub available: bool, + + /// Whether the algorithm produces byte-identical output for the same + /// key/nonce/plaintext across providers. + /// + /// Must be `true` for any algorithm used in the shared cipher chain. + /// Cross-provider conformance tests verify this at build time. + pub deterministic_io: bool, + + /// Throughput score normalised to 0–100 (0 = slowest, 100 = fastest). + /// + /// Populated after `WolfSslProvider::benchmark()` runs at app start. + /// Stored so Dart can rank providers when multiple are available. + pub efficiency_score: u8, + + /// Reliability score normalised to 0–100. + /// + /// Derived from the self-test pass rate: `(passed / total) * 100`. + pub reliability_score: u8, +} + +impl AlgorithmCapability { + /// Create a placeholder capability that marks an algorithm as unavailable. + /// + /// Used while the provider is initialising or when a probe fails. + pub fn unavailable() -> Self { + Self { + available: false, + deterministic_io: false, + efficiency_score: 0, + reliability_score: 0, + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── + +/// The complete runtime capability matrix for one crypto provider. +/// +/// Returned by [`CryptoProvider::capabilities()`] and forwarded to Dart as +/// `CapabilitiesDto`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderCapabilities { + /// The provider's canonical name (matches `CccCryptoProvider` enum in Dart). + pub provider_name: String, + + /// AEAD algorithm capabilities, keyed by [`AeadAlgorithm`]. + pub aead: HashMap, + + /// KDF algorithm capabilities, keyed by [`KdfAlgorithm`]. + pub kdf: HashMap, + + /// MAC algorithm capabilities, keyed by [`MacAlgorithm`]. + pub mac: HashMap, + + /// Hash algorithm capabilities, keyed by [`HashAlgorithm`]. + pub hash: HashMap, + + /// KEM algorithm capabilities, keyed by [`KemAlgorithm`]. + pub kem: HashMap, +} + +impl ProviderCapabilities { + /// Create an empty capability set for the given provider. + pub fn empty(provider_name: impl Into) -> Self { + Self { + provider_name: provider_name.into(), + aead: HashMap::new(), + kdf: HashMap::new(), + mac: HashMap::new(), + hash: HashMap::new(), + kem: HashMap::new(), + } + } + + /// Register an AEAD capability entry. + pub fn with_aead(mut self, algo: AeadAlgorithm, cap: AlgorithmCapability) -> Self { + self.aead.insert(algo, cap); + self + } + + /// Register a KDF capability entry. + pub fn with_kdf(mut self, algo: KdfAlgorithm, cap: AlgorithmCapability) -> Self { + self.kdf.insert(algo, cap); + self + } + + /// Register a MAC capability entry. + pub fn with_mac(mut self, algo: MacAlgorithm, cap: AlgorithmCapability) -> Self { + self.mac.insert(algo, cap); + self + } + + /// Register a Hash capability entry. + pub fn with_hash(mut self, algo: HashAlgorithm, cap: AlgorithmCapability) -> Self { + self.hash.insert(algo, cap); + self + } + + /// Register a KEM capability entry. + pub fn with_kem(mut self, algo: KemAlgorithm, cap: AlgorithmCapability) -> Self { + self.kem.insert(algo, cap); + self + } + + /// Return `true` if any algorithm in any category is available. + pub fn has_any_available(&self) -> bool { + self.aead.values().any(|c| c.available) + || self.kdf.values().any(|c| c.available) + || self.mac.values().any(|c| c.available) + || self.hash.values().any(|c| c.available) + || self.kem.values().any(|c| c.available) + } +} diff --git a/crates/ccc-crypto-core/src/error.rs b/crates/ccc-crypto-core/src/error.rs new file mode 100644 index 0000000..700a2cb --- /dev/null +++ b/crates/ccc-crypto-core/src/error.rs @@ -0,0 +1,46 @@ +//! Unified error type for all CCC crypto operations. + +use thiserror::Error; + +/// All errors that can arise from CCC crypto provider operations. +/// +/// Every provider trait method returns `Result<_, CryptoError>`. The bridge +/// crate converts this into a Dart `Result` / exception. +#[derive(Debug, Error)] +pub enum CryptoError { + /// The requested algorithm is not supported by this provider or is not + /// available on the current platform build. + #[error("unsupported algorithm: {0}")] + UnsupportedAlgorithm(String), + + /// The provided key has the wrong length or invalid content. + #[error("invalid key: {0}")] + InvalidKey(String), + + /// The provided nonce has the wrong length. + #[error("invalid nonce: {0}")] + InvalidNonce(String), + + /// AEAD / MAC authentication tag verification failed. + /// + /// This is the only error that should be surfaced to end users (as a + /// generic "decryption failed" message; do not leak details). + #[error("authentication failed")] + AuthenticationFailed, + + /// The input data is malformed or has wrong length. + #[error("invalid input: {0}")] + InvalidInput(String), + + /// The algorithm requires a feature (e.g. PQ build) that is not compiled + /// into the native library. + #[error("feature not compiled: {0}")] + FeatureNotCompiled(String), + + /// An unexpected internal error from the underlying native library. + /// + /// The `String` contains library-specific diagnostic text; do not surface + /// this to end users. + #[error("internal error: {0}")] + InternalError(String), +} diff --git a/crates/ccc-crypto-core/src/lib.rs b/crates/ccc-crypto-core/src/lib.rs new file mode 100644 index 0000000..6c5233c --- /dev/null +++ b/crates/ccc-crypto-core/src/lib.rs @@ -0,0 +1,24 @@ +//! # ccc-crypto-core +//! +//! Shared algorithm enumerations, capability structs, provider traits, and the +//! global [`ProviderRegistry`] for the CCC (Copious Cipher Chain) cryptography +//! system. +//! +//! All algorithm discriminant values match the integer constants defined in +//! `cipher_constants.dart` exactly, so the Dart cipher-sequencing logic +//! requires zero changes when calling into Rust. + +pub mod algorithms; +pub mod capabilities; +pub mod error; +pub mod provider; +pub mod registry; +pub mod types; + +// Re-export everything a provider crate or the bridge crate needs. +pub use algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}; +pub use capabilities::{AlgorithmCapability, ProviderCapabilities}; +pub use error::CryptoError; +pub use provider::{AeadProvider, CryptoProvider, HashProvider, KdfProvider, KemProvider, MacProvider}; +pub use registry::ProviderRegistry; +pub use types::{AlgoTestResult, BenchmarkReport, KemEncapResult, KemKeyPair, SelfTestReport}; diff --git a/crates/ccc-crypto-core/src/provider.rs b/crates/ccc-crypto-core/src/provider.rs new file mode 100644 index 0000000..e89dcf3 --- /dev/null +++ b/crates/ccc-crypto-core/src/provider.rs @@ -0,0 +1,197 @@ +//! Provider traits that every crypto backend must implement. +//! +//! A concrete provider (e.g. `WolfSslProvider`) implements [`CryptoProvider`], +//! which in turn requires all five primitive traits: [`AeadProvider`], +//! [`KdfProvider`], [`MacProvider`], [`HashProvider`], and optionally +//! [`KemProvider`]. +//! +//! The trait objects are `Send + Sync` so they can live in the global +//! [`ProviderRegistry`][crate::registry::ProviderRegistry] behind a +//! `Mutex`. + +use zeroize::Zeroizing; + +use crate::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}, + capabilities::ProviderCapabilities, + error::CryptoError, + types::{BenchmarkReport, KemEncapResult, KemKeyPair, SelfTestReport}, +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Primitive traits +// ────────────────────────────────────────────────────────────────────────────── + +/// AEAD (Authenticated Encryption with Associated Data) operations. +pub trait AeadProvider { + /// Encrypt `plaintext` and return `ciphertext || tag`. + /// + /// # Parameters + /// - `algo` – Which AEAD algorithm to use. + /// - `key` – Raw key bytes. Length must match [`AeadAlgorithm::key_len`]. + /// - `nonce` – Nonce / IV bytes. Length must match [`AeadAlgorithm::nonce_len`]. + /// - `plaintext` – Data to encrypt. + /// - `aad` – Additional associated data (authenticated but not encrypted). + /// + /// # Returns + /// Concatenated ciphertext + authentication tag. + fn encrypt_aead( + &self, + algo: AeadAlgorithm, + key: &[u8], + nonce: &[u8], + plaintext: &[u8], + aad: &[u8], + ) -> Result, CryptoError>; + + /// Decrypt and authenticate `ciphertext || tag`, returning the plaintext. + /// + /// Returns [`CryptoError::AuthenticationFailed`] if the tag does not verify. + fn decrypt_aead( + &self, + algo: AeadAlgorithm, + key: &[u8], + nonce: &[u8], + ciphertext_and_tag: &[u8], + aad: &[u8], + ) -> Result, CryptoError>; +} + +/// Key derivation function operations. +pub trait KdfProvider { + /// Derive a key of `length` bytes from input keying material. + /// + /// # Parameters + /// - `algo` – Which KDF to use. + /// - `ikm` – Input keying material. + /// - `salt` – Optional salt (pass an empty slice to use the zero salt). + /// - `info` – Context / label bytes (HKDF `info` field or equivalent). + /// - `length` – Desired output byte length. + /// + /// # Returns + /// A `Zeroizing>` that is zeroed automatically on drop. + fn derive_key( + &self, + algo: KdfAlgorithm, + ikm: &[u8], + salt: &[u8], + info: &[u8], + length: usize, + ) -> Result>, CryptoError>; +} + +/// Message authentication code operations. +pub trait MacProvider { + /// Compute a MAC tag over `data` using `key`. + fn compute_mac( + &self, + algo: MacAlgorithm, + key: &[u8], + data: &[u8], + ) -> Result, CryptoError>; + + /// Verify that `mac` is the correct tag for `data` under `key`. + /// + /// Implementations **must** use constant-time comparison to avoid timing + /// side-channels. + /// + /// Returns `Ok(true)` if verification succeeds, `Ok(false)` if the tag + /// does not match (do not return an error for mismatches — the caller + /// decides how to handle false). + fn verify_mac( + &self, + algo: MacAlgorithm, + key: &[u8], + data: &[u8], + mac: &[u8], + ) -> Result; +} + +/// Cryptographic hash operations. +pub trait HashProvider { + /// Compute a digest of `data` using the specified algorithm. + fn hash( + &self, + algo: HashAlgorithm, + data: &[u8], + ) -> Result, CryptoError>; +} + +/// Key encapsulation mechanism operations. +/// +/// Not required by [`CryptoProvider`] — providers that only implement symmetric +/// operations simply do not implement this trait. The bridge exposes KEM +/// functions separately and looks up a `KemProvider` from the registry. +pub trait KemProvider { + /// Generate a new KEM key pair. + fn generate_keypair( + &self, + algo: KemAlgorithm, + ) -> Result; + + /// Encapsulate a shared secret for the holder of `public_key`. + /// + /// Returns `(ciphertext, shared_secret)`. The caller sends `ciphertext` + /// to the peer and uses `shared_secret` locally. + fn encapsulate( + &self, + algo: KemAlgorithm, + public_key: &[u8], + ) -> Result; + + /// Decapsulate a shared secret from `ciphertext` using `private_key`. + /// + /// Returns the same `shared_secret` that the encapsulator obtained. + fn decapsulate( + &self, + algo: KemAlgorithm, + private_key: &[u8], + ciphertext: &[u8], + ) -> Result>, CryptoError>; +} + +// ────────────────────────────────────────────────────────────────────────────── +// Umbrella trait +// ────────────────────────────────────────────────────────────────────────────── + +/// The combined interface every full-featured crypto provider must satisfy. +/// +/// Implementors are automatically usable as `Box` in the +/// [`ProviderRegistry`][crate::registry::ProviderRegistry]. +/// +/// # Implementing a new provider +/// +/// 1. Create a new crate under `crates/ccc-crypto-/`. +/// 2. Implement `AeadProvider`, `KdfProvider`, `MacProvider`, `HashProvider` +/// for your provider struct. +/// 3. Implement `CryptoProvider` (add `capabilities()`, `self_test()`, +/// `benchmark()`, `provider_name()`). +/// 4. In your `init()` function call +/// `ProviderRegistry::global().register(name, Box::new(provider))`. +/// 5. Call `ccc_crypto_::init()` from `ccc-flutter-bridge/src/lib.rs`. +/// 6. Add NIST test vectors for every algorithm you expose. +pub trait CryptoProvider: AeadProvider + KdfProvider + MacProvider + HashProvider + Send + Sync { + /// The provider's canonical name (e.g. `"wolfssl"`). + /// + /// Must be lowercase, no spaces, match the `CccCryptoProvider` enum value + /// in `ccc_provider_spec.dart`. + fn provider_name(&self) -> &'static str; + + /// Return the full runtime capability matrix for this provider. + /// + /// Called once at startup after [`CryptoProvider::self_test`] and + /// [`CryptoProvider::benchmark`] have completed. + fn capabilities(&self) -> ProviderCapabilities; + + /// Run embedded NIST/RFC test vectors for every implemented algorithm. + /// + /// This must be a deterministic, side-effect-free operation that can be + /// called at any time (including in CI without native hardware). + fn self_test(&self) -> SelfTestReport; + + /// Measure throughput for every implemented AEAD algorithm. + /// + /// Encrypt 1 MB of random data × 100 iterations and normalise the + /// result into `efficiency_score: u8` (0–100). + fn benchmark(&self) -> BenchmarkReport; +} diff --git a/crates/ccc-crypto-core/src/registry.rs b/crates/ccc-crypto-core/src/registry.rs new file mode 100644 index 0000000..325b58d --- /dev/null +++ b/crates/ccc-crypto-core/src/registry.rs @@ -0,0 +1,237 @@ +//! Global provider registry. +//! +//! [`ProviderRegistry`] is a lazily-initialised, thread-safe map from provider +//! name to a boxed [`CryptoProvider`]. Each provider crate's `init()` function +//! registers its implementation here; the bridge crate calls those `init()` +//! functions from `ccc_init()`. +//! +//! # Example +//! +//! ```rust,ignore +//! // In ccc-crypto-wolfssl: +//! pub fn init() { +//! ProviderRegistry::global().register("wolfssl", Box::new(WolfSslProvider::new())); +//! } +//! +//! // In the bridge: +//! pub fn ccc_init() { +//! ccc_crypto_wolfssl::init(); +//! } +//! +//! // Anywhere: +//! let registry = ProviderRegistry::global(); +//! let provider = registry.get("wolfssl").expect("wolfssl not registered"); +//! let ct = provider.encrypt_aead(...)?; +//! ``` + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use once_cell::sync::OnceCell; + +use crate::provider::CryptoProvider; + +// ────────────────────────────────────────────────────────────────────────────── + +/// Thread-safe registry mapping provider names to boxed [`CryptoProvider`] +/// trait objects. +pub struct ProviderRegistry { + inner: Mutex>>, +} + +impl ProviderRegistry { + /// Return the global singleton registry. + /// + /// Initialised on first call. Safe to call from any thread. + pub fn global() -> &'static Self { + static INSTANCE: OnceCell = OnceCell::new(); + INSTANCE.get_or_init(|| ProviderRegistry { + inner: Mutex::new(HashMap::new()), + }) + } + + /// Register a provider under `name`. + /// + /// If a provider with the same name already exists it is replaced. + /// `name` must be lowercase, no spaces (e.g. `"wolfssl"`). + /// + /// # Panics + /// + /// Panics if the internal mutex is poisoned (indicates an earlier panic + /// in a thread holding the lock — treat as unrecoverable). + pub fn register(&self, name: &str, provider: Box) { + let mut map = self.inner.lock().expect("ProviderRegistry mutex poisoned"); + log::info!("[ccc-crypto] registered provider '{}'", name); + map.insert(name.to_string(), Arc::from(provider)); + } + + /// Retrieve a provider by name. + /// + /// Returns `None` if no provider with that name has been registered. + pub fn get(&self, name: &str) -> Option> { + let map = self.inner.lock().expect("ProviderRegistry mutex poisoned"); + map.get(name).cloned() + } + + /// Return the list of all registered provider names. + pub fn list(&self) -> Vec { + let map = self.inner.lock().expect("ProviderRegistry mutex poisoned"); + map.keys().cloned().collect() + } + + /// Return `true` if the named provider has been registered. + pub fn contains(&self, name: &str) -> bool { + let map = self.inner.lock().expect("ProviderRegistry mutex poisoned"); + map.contains_key(name) + } + + /// Remove a provider from the registry. + /// + /// Primarily for use in tests. + pub fn unregister(&self, name: &str) { + let mut map = self.inner.lock().expect("ProviderRegistry mutex poisoned"); + map.remove(name); + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + //! Registry unit tests use a stub provider to avoid any native dependency. + + use super::*; + use crate::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, MacAlgorithm}, + capabilities::ProviderCapabilities, + error::CryptoError, + provider::{AeadProvider, CryptoProvider, HashProvider, KdfProvider, MacProvider}, + types::{AlgoBenchResult, AlgoTestResult, BenchmarkReport, SelfTestReport}, + }; + use zeroize::Zeroizing; + + // ── Stub provider ────────────────────────────────────────────────────────── + + struct StubProvider; + + impl AeadProvider for StubProvider { + fn encrypt_aead( + &self, _a: AeadAlgorithm, _k: &[u8], _n: &[u8], pt: &[u8], _aad: &[u8], + ) -> Result, CryptoError> { + Ok(pt.to_vec()) + } + fn decrypt_aead( + &self, _a: AeadAlgorithm, _k: &[u8], _n: &[u8], ct: &[u8], _aad: &[u8], + ) -> Result, CryptoError> { + Ok(ct.to_vec()) + } + } + + impl KdfProvider for StubProvider { + fn derive_key( + &self, _a: KdfAlgorithm, _ikm: &[u8], _salt: &[u8], _info: &[u8], length: usize, + ) -> Result>, CryptoError> { + Ok(Zeroizing::new(vec![0u8; length])) + } + } + + impl MacProvider for StubProvider { + fn compute_mac( + &self, _a: MacAlgorithm, _k: &[u8], _data: &[u8], + ) -> Result, CryptoError> { + Ok(vec![0u8; 32]) + } + fn verify_mac( + &self, _a: MacAlgorithm, _k: &[u8], _data: &[u8], _mac: &[u8], + ) -> Result { + Ok(true) + } + } + + impl HashProvider for StubProvider { + fn hash( + &self, _a: HashAlgorithm, data: &[u8], + ) -> Result, CryptoError> { + Ok(data.to_vec()) + } + } + + impl CryptoProvider for StubProvider { + fn provider_name(&self) -> &'static str { "stub" } + fn capabilities(&self) -> ProviderCapabilities { + ProviderCapabilities::empty("stub") + } + fn self_test(&self) -> SelfTestReport { + SelfTestReport::finalise("stub", vec![AlgoTestResult { + algo_id: 12, algo_name: "stub".into(), passed: true, error_message: None, + }]) + } + fn benchmark(&self) -> BenchmarkReport { + BenchmarkReport { + provider_name: "stub".into(), + results: vec![AlgoBenchResult { + algo_id: 12, algo_name: "stub".into(), + throughput_mbps: 999.0, efficiency_score: 100, + }], + } + } + } + + // ── Tests ────────────────────────────────────────────────────────────────── + + fn fresh_registry() -> ProviderRegistry { + ProviderRegistry { inner: Mutex::new(HashMap::new()) } + } + + #[test] + fn register_and_get() { + let reg = fresh_registry(); + assert!(reg.get("stub").is_none()); + reg.register("stub", Box::new(StubProvider)); + assert!(reg.get("stub").is_some()); + } + + #[test] + fn list_providers() { + let reg = fresh_registry(); + assert!(reg.list().is_empty()); + reg.register("stub", Box::new(StubProvider)); + let names = reg.list(); + assert_eq!(names.len(), 1); + assert_eq!(names[0], "stub"); + } + + #[test] + fn contains_and_unregister() { + let reg = fresh_registry(); + reg.register("stub", Box::new(StubProvider)); + assert!(reg.contains("stub")); + reg.unregister("stub"); + assert!(!reg.contains("stub")); + } + + #[test] + fn register_replaces_existing() { + let reg = fresh_registry(); + reg.register("stub", Box::new(StubProvider)); + reg.register("stub", Box::new(StubProvider)); // replace + assert_eq!(reg.list().len(), 1); + } + + #[test] + fn stub_provider_roundtrip() { + let reg = fresh_registry(); + reg.register("stub", Box::new(StubProvider)); + let provider = reg.get("stub").unwrap(); + let plaintext = b"hello world"; + let ct = provider + .encrypt_aead(AeadAlgorithm::AesGcm256, &[0u8; 32], &[0u8; 12], plaintext, b"") + .unwrap(); + let pt = provider + .decrypt_aead(AeadAlgorithm::AesGcm256, &[0u8; 32], &[0u8; 12], &ct, b"") + .unwrap(); + assert_eq!(pt, plaintext); + } +} diff --git a/crates/ccc-crypto-core/src/types.rs b/crates/ccc-crypto-core/src/types.rs new file mode 100644 index 0000000..91bcb0a --- /dev/null +++ b/crates/ccc-crypto-core/src/types.rs @@ -0,0 +1,98 @@ +//! Shared value types used across provider traits and the bridge API. + +use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +// ────────────────────────────────────────────────────────────────────────────── +// KEM types +// ────────────────────────────────────────────────────────────────────────────── + +/// A KEM key pair returned by [`KemProvider::generate_keypair`]. +/// +/// Both key buffers are zeroed automatically when dropped. +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct KemKeyPair { + /// The public key (safe to transmit to the peer). + pub public_key: Vec, + /// The private key (must never leave the device). + pub private_key: Vec, +} + +/// The result of a KEM encapsulation operation. +/// +/// The shared secret is zeroed when dropped. The ciphertext is public. +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct KemEncapResult { + /// The ciphertext to send to the holder of the private key. + pub ciphertext: Vec, + /// The shared secret derived locally. + pub shared_secret: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// Self-test and benchmark report types +// ────────────────────────────────────────────────────────────────────────────── + +/// The outcome of a single algorithm self-test. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AlgoTestResult { + /// Algorithm discriminant (same as the `u32` value passed over FFI). + pub algo_id: u32, + /// Human-readable algorithm name. + pub algo_name: String, + /// Whether the test vector verification passed. + pub passed: bool, + /// Error message if `passed` is `false`; `None` otherwise. + pub error_message: Option, +} + +/// The complete self-test report for one provider. +/// +/// Produced by [`CryptoProvider::self_test`] and forwarded to Dart via +/// `ccc_self_test()`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SelfTestReport { + /// Provider name. + pub provider_name: String, + /// Per-algorithm test results. + pub results: Vec, + /// `true` if and only if every result in `results` has `passed == true`. + pub all_passed: bool, +} + +impl SelfTestReport { + /// Compute `all_passed` from the results list and return a finalised report. + pub fn finalise(provider_name: impl Into, results: Vec) -> Self { + let all_passed = results.iter().all(|r| r.passed); + Self { + provider_name: provider_name.into(), + results, + all_passed, + } + } +} + +/// Per-algorithm throughput measurement from the provider benchmark. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AlgoBenchResult { + /// Algorithm discriminant. + pub algo_id: u32, + /// Human-readable algorithm name. + pub algo_name: String, + /// Measured throughput in MB/s (1 MB × 100 iterations). + pub throughput_mbps: f64, + /// Normalised score 0–100 (relative to the fastest algorithm measured). + pub efficiency_score: u8, +} + +/// The complete benchmark report for one provider. +/// +/// Produced by [`CryptoProvider::benchmark`]. Results are used to populate +/// `AlgorithmCapability::efficiency_score` in [`ProviderCapabilities`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BenchmarkReport { + /// Provider name. + pub provider_name: String, + /// Per-algorithm benchmark results. + pub results: Vec, +} diff --git a/crates/ccc-crypto-wolfssl/Cargo.toml b/crates/ccc-crypto-wolfssl/Cargo.toml new file mode 100644 index 0000000..a390a37 --- /dev/null +++ b/crates/ccc-crypto-wolfssl/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ccc-crypto-wolfssl" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "wolfSSL / wolfCrypt provider for the CCC crypto system." + +[dependencies] +ccc-crypto-core.workspace = true +zeroize.workspace = true +thiserror.workspace = true +log.workspace = true + +[build-dependencies] +# cmake crate drives the wolfSSL CMake build from source. +cmake = "0.1" +# bindgen generates Rust FFI bindings to the wolfCrypt C headers. +bindgen = "0.72" + +[dev-dependencies] +# hex for decoding NIST test vector strings in tests. +hex = "0.4" + +[features] +# Enable this when building without the wolfSSL C library +# (e.g. on docs.rs, or for type-checking in CI without cmake). +stub_ffi = [] diff --git a/crates/ccc-crypto-wolfssl/build.rs b/crates/ccc-crypto-wolfssl/build.rs new file mode 100644 index 0000000..b11d333 --- /dev/null +++ b/crates/ccc-crypto-wolfssl/build.rs @@ -0,0 +1,204 @@ +//! Build script for ccc-crypto-wolfssl. +//! +//! Responsibilities: +//! 1. Locate the wolfSSL source tree (vendored submodule). +//! 2. Build wolfSSL as a static library using CMake. +//! 3. Generate Rust FFI bindings to the wolfCrypt headers via bindgen. +//! +//! # Environment variables +//! +//! * `WOLFSSL_SOURCE_DIR` – Override the default wolfSSL source path. +//! Useful for CI environments where the submodule may be at a non-standard +//! location. Defaults to `/vendors/wolfssl`. +//! * `WOLFSSL_INSTALL_DIR` – If set, skip the CMake build and link against +//! a pre-installed wolfSSL at this path. The path must contain +//! `include/wolfssl/` and `lib/libwolfssl.a`. + +use std::{env, path::PathBuf}; + +fn main() { + // When the `stub_ffi` feature is enabled (e.g. for type-checking without + // the wolfSSL C library), skip all build steps and write an empty bindings + // file so the `include!()` in lib.rs still compiles. + if std::env::var("CARGO_FEATURE_STUB_FFI").is_ok() { + let out_dir = std::env::var("OUT_DIR").unwrap(); + let bindings_path = std::path::Path::new(&out_dir).join("wolfcrypt_bindings.rs"); + std::fs::write(&bindings_path, "// stub — no wolfSSL headers\n").unwrap(); + return; + } + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + // Workspace root is two levels up from crates/ccc-crypto-wolfssl/ + let workspace_root = manifest_dir.join("../..").canonicalize().unwrap(); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + // ── 1. Locate wolfSSL ──────────────────────────────────────────────────── + + let (include_dir, lib_dir) = + if let Ok(install_dir) = env::var("WOLFSSL_INSTALL_DIR") { + // Use a pre-installed wolfSSL — skip the CMake build. + let p = PathBuf::from(&install_dir); + println!("cargo:warning=Using pre-installed wolfSSL at {}", install_dir); + (p.join("include"), p.join("lib")) + } else { + // Build from source (default — uses our git submodule). + let source_dir = env::var("WOLFSSL_SOURCE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_root.join("vendors/wolfssl")); + + if !source_dir.join("CMakeLists.txt").exists() { + panic!( + "\n\nwolfSSL source not found at {}.\n\ + Run `git submodule update --init --recursive` to fetch it,\n\ + or set WOLFSSL_SOURCE_DIR to an alternative path.\n", + source_dir.display() + ); + } + + build_wolfssl_cmake(&source_dir, &out_dir) + }; + + // ── 2. Tell Cargo how to link ──────────────────────────────────────────── + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=static=wolfssl"); + + // Link system libraries required by wolfSSL on each platform. + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + match target_os.as_str() { + "macos" | "ios" => { + println!("cargo:rustc-link-lib=framework=Security"); + println!("cargo:rustc-link-lib=c++"); + } + "linux" | "android" => { + println!("cargo:rustc-link-lib=stdc++"); + } + _ => {} + } + + // Re-run the build script if any of these change. + println!("cargo:rerun-if-env-changed=WOLFSSL_SOURCE_DIR"); + println!("cargo:rerun-if-env-changed=WOLFSSL_INSTALL_DIR"); + + // ── 3. Generate FFI bindings via bindgen ───────────────────────────────── + + generate_bindings(&include_dir, &out_dir); +} + +/// Build wolfSSL from source using CMake. Returns `(include_dir, lib_dir)`. +fn build_wolfssl_cmake(source_dir: &PathBuf, out_dir: &PathBuf) -> (PathBuf, PathBuf) { + println!( + "cargo:warning=Building wolfSSL from source at {}", + source_dir.display() + ); + + let mut cfg = cmake::Config::new(source_dir); + + // ── Core algorithm selection ───────────────────────────────────────────── + // Enable exactly the algorithms the CCC system needs in Phase 4. + // Additional algorithms can be enabled here for Phase 5+ PQ support. + cfg + // Build as a static library. + .define("BUILD_SHARED_LIBS", "OFF") + .define("WOLFSSL_BUILD_SHARED_LIBS", "OFF") + // Disable TLS/SSL stack — we only need the wolfCrypt algorithms. + .define("WOLFSSL_NO_TLS", "ON") + // AEAD + .define("WOLFSSL_AES_GCM", "ON") + .define("WOLFSSL_CHACHA", "ON") + .define("WOLFSSL_POLY1305", "ON") + // Hash + .define("WOLFSSL_SHA224", "ON") + .define("WOLFSSL_SHA384", "ON") + .define("WOLFSSL_SHA512", "ON") + .define("WOLFSSL_SHA3", "ON") + .define("WOLFSSL_BLAKE2", "ON") + // KDF + .define("WOLFSSL_HKDF", "ON") + .define("WOLFSSL_PWDBASED", "ON") // PBKDF2, Argon2 + // MAC + .define("WOLFSSL_HMAC", "ON") + // Asymmetric (needed for X25519 ratchet) + .define("WOLFSSL_CURVE25519", "ON") + .define("WOLFSSL_CURVE448", "ON") + // RNG (needed internally) + .define("WOLFSSL_RNG", "ON") + // Minimise binary size. + .define("WOLFSSL_CRYPTONLY", "ON") + .define("WOLFSSL_MIN_RSA_BITS", "2048") + .define("WOLFSSL_NO_FILESYSTEM", "ON"); + + let install_path = cfg.build(); + + let include_dir = install_path.join("include"); + let lib_dir = install_path.join("lib"); + + (include_dir, lib_dir) +} + +/// Run bindgen over the wolfCrypt headers we actually use. +fn generate_bindings(include_dir: &PathBuf, out_dir: &PathBuf) { + let header = include_dir.join("wolfssl/wolfcrypt/aes.h"); + + if !header.exists() { + // Bindings are only generated when wolfSSL headers are present. + // In environments without the C library (e.g. pure Rust CI), a + // pre-generated bindings file can be committed at src/bindings.rs. + println!( + "cargo:warning=wolfSSL headers not found at {}; \ + using pre-generated bindings if available.", + include_dir.display() + ); + return; + } + + let bindings = bindgen::Builder::default() + // Include all wolfCrypt headers we need. + .header(include_dir.join("wolfssl/wolfcrypt/aes.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/chacha20_poly1305.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/sha256.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/sha512.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/sha3.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/blake2.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/hmac.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/kdf.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/curve25519.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/curve448.h").to_str().unwrap()) + .header(include_dir.join("wolfssl/wolfcrypt/random.h").to_str().unwrap()) + .clang_arg(format!("-I{}", include_dir.display())) + // Only generate bindings for the types/functions we whitelist. + .allowlist_function("wc_Aes.*") + .allowlist_function("wc_ChaCha20Poly1305.*") + .allowlist_function("wc_Sha.*") + .allowlist_function("wc_Blake2.*") + .allowlist_function("wc_Hmac.*") + .allowlist_function("wc_HKDF.*") + .allowlist_function("wc_Pbkdf2.*") + .allowlist_function("wc_Argon2.*") + .allowlist_function("wc_curve25519.*") + .allowlist_function("wc_curve448.*") + .allowlist_function("wc_InitRng.*") + .allowlist_function("wc_RNG.*") + .allowlist_function("wc_FreeRng.*") + .allowlist_type("Aes") + .allowlist_type("Hmac") + .allowlist_type("WC_RNG") + .allowlist_type("curve25519.*") + .allowlist_type("curve448.*") + .allowlist_var("AES_.*") + .allowlist_var("WC_.*") + .allowlist_var("SHA.*") + .allowlist_var("BLAKE2.*") + // Silence warnings for types we don't control. + .blocklist_type("__.*") + .derive_debug(false) + .layout_tests(false) + .generate() + .expect("bindgen failed to generate wolfCrypt bindings"); + + bindings + .write_to_file(out_dir.join("wolfcrypt_bindings.rs")) + .expect("failed to write wolfCrypt bindings"); + + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/crates/ccc-crypto-wolfssl/src/aead.rs b/crates/ccc-crypto-wolfssl/src/aead.rs new file mode 100644 index 0000000..61833ce --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/aead.rs @@ -0,0 +1,303 @@ +//! AEAD (Authenticated Encryption with Associated Data) implementations. +//! +//! Provides AES-256-GCM and ChaCha20-Poly1305 via wolfCrypt. +//! +//! # Wire format (matches ccc_iso.dart) +//! +//! The output of [`encrypt`] is: `ciphertext || tag (16 bytes)`. +//! This byte layout is identical to the Dart implementation in `ccc_iso.dart` +//! so that cross-provider conformance tests pass. + +use ccc_crypto_core::{algorithms::AeadAlgorithm, error::CryptoError}; + +// ────────────────────────────────────────────────────────────────────────────── +// Public entry points (called from WolfSslProvider) +// ────────────────────────────────────────────────────────────────────────────── + +/// Encrypt using the specified AEAD algorithm. +/// +/// Output: `ciphertext || 16-byte auth tag`. +pub fn encrypt( + algo: AeadAlgorithm, + key: &[u8], + nonce: &[u8], + plaintext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + validate_key_nonce(algo, key, nonce)?; + match algo { + AeadAlgorithm::AesGcm256 => aes_gcm_256_encrypt(key, nonce, plaintext, aad), + AeadAlgorithm::ChaCha20Poly1305 => chacha20_poly1305_encrypt(key, nonce, plaintext, aad), + AeadAlgorithm::XChaCha20Poly1305 => xchacha20_poly1305_encrypt(key, nonce, plaintext, aad), + AeadAlgorithm::Ascon128a => + Err(CryptoError::FeatureNotCompiled("Ascon-128a".into())), + } +} + +/// Decrypt using the specified AEAD algorithm. +/// +/// Input: `ciphertext || 16-byte auth tag`. Returns plaintext or +/// [`CryptoError::AuthenticationFailed`] if the tag does not verify. +pub fn decrypt( + algo: AeadAlgorithm, + key: &[u8], + nonce: &[u8], + ciphertext_and_tag: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + validate_key_nonce(algo, key, nonce)?; + match algo { + AeadAlgorithm::AesGcm256 => aes_gcm_256_decrypt(key, nonce, ciphertext_and_tag, aad), + AeadAlgorithm::ChaCha20Poly1305 => chacha20_poly1305_decrypt(key, nonce, ciphertext_and_tag, aad), + AeadAlgorithm::XChaCha20Poly1305 => xchacha20_poly1305_decrypt(key, nonce, ciphertext_and_tag, aad), + AeadAlgorithm::Ascon128a => + Err(CryptoError::FeatureNotCompiled("Ascon-128a".into())), + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Validation helpers +// ────────────────────────────────────────────────────────────────────────────── + +fn validate_key_nonce(algo: AeadAlgorithm, key: &[u8], nonce: &[u8]) -> Result<(), CryptoError> { + let expected_key = algo.key_len(); + let expected_nonce = algo.nonce_len(); + if key.len() != expected_key { + return Err(CryptoError::InvalidKey(format!( + "{}: expected {}-byte key, got {}", + algo.name(), expected_key, key.len() + ))); + } + if nonce.len() != expected_nonce { + return Err(CryptoError::InvalidNonce(format!( + "{}: expected {}-byte nonce, got {}", + algo.name(), expected_nonce, nonce.len() + ))); + } + Ok(()) +} + +const TAG_LEN: usize = 16; + +// ────────────────────────────────────────────────────────────────────────────── +// AES-256-GCM +// ────────────────────────────────────────────────────────────────────────────── + +fn aes_gcm_256_encrypt( + key: &[u8], + nonce: &[u8], // 12 bytes + plaintext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + let mut out = vec![0u8; plaintext.len() + TAG_LEN]; + let (ct_buf, tag_buf) = out.split_at_mut(plaintext.len()); + + unsafe { + // wolfCrypt AES-GCM encrypt: + // wc_AesGcmEncrypt(aes, out, input, sz, iv, ivSz, authTag, authTagSz, authIn, authInSz) + // + // We use the one-shot wc_AesGcmEncrypt which handles Aes struct init internally + // via wc_AesGcmSetKey. + let mut aes: crate::sys::Aes = std::mem::zeroed(); + + let ret = crate::sys::wc_AesGcmSetKey( + &mut aes, + key.as_ptr(), + key.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InvalidKey(format!("wc_AesGcmSetKey returned {}", ret))); + } + + let ret = crate::sys::wc_AesGcmEncrypt( + &mut aes, + ct_buf.as_mut_ptr(), + plaintext.as_ptr(), + plaintext.len() as u32, + nonce.as_ptr(), + nonce.len() as u32, + tag_buf.as_mut_ptr(), + TAG_LEN as u32, + if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() }, + aad.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_AesGcmEncrypt returned {}", ret))); + } + } + + Ok(out) +} + +fn aes_gcm_256_decrypt( + key: &[u8], + nonce: &[u8], + ciphertext_and_tag: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + if ciphertext_and_tag.len() < TAG_LEN { + return Err(CryptoError::InvalidInput( + "AES-256-GCM: ciphertext too short (no room for tag)".into() + )); + } + let ct_len = ciphertext_and_tag.len() - TAG_LEN; + let (ciphertext, tag) = ciphertext_and_tag.split_at(ct_len); + let mut plaintext = vec![0u8; ct_len]; + + unsafe { + let mut aes: crate::sys::Aes = std::mem::zeroed(); + + let ret = crate::sys::wc_AesGcmSetKey( + &mut aes, + key.as_ptr(), + key.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InvalidKey(format!("wc_AesGcmSetKey returned {}", ret))); + } + + let ret = crate::sys::wc_AesGcmDecrypt( + &mut aes, + plaintext.as_mut_ptr(), + ciphertext.as_ptr(), + ct_len as u32, + nonce.as_ptr(), + nonce.len() as u32, + tag.as_ptr(), + TAG_LEN as u32, + if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() }, + aad.len() as u32, + ); + if ret != 0 { + // wolfCrypt returns AES_GCM_AUTH_E (-180) on auth failure. + return Err(CryptoError::AuthenticationFailed); + } + } + + Ok(plaintext) +} + +// ────────────────────────────────────────────────────────────────────────────── +// ChaCha20-Poly1305 (12-byte nonce) +// ────────────────────────────────────────────────────────────────────────────── + +fn chacha20_poly1305_encrypt( + key: &[u8], + nonce: &[u8], // 12 bytes + plaintext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + let mut out = vec![0u8; plaintext.len() + TAG_LEN]; + let (ct_buf, tag_buf) = out.split_at_mut(plaintext.len()); + + unsafe { + // wc_ChaCha20Poly1305_Encrypt(key, nonce, aad, aadSz, input, inputSz, output, authTag) + let ret = crate::sys::wc_ChaCha20Poly1305_Encrypt( + key.as_ptr(), + nonce.as_ptr(), + if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() }, + aad.len() as u32, + plaintext.as_ptr(), + plaintext.len() as u32, + ct_buf.as_mut_ptr(), + tag_buf.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError( + format!("wc_ChaCha20Poly1305_Encrypt returned {}", ret) + )); + } + } + + Ok(out) +} + +fn chacha20_poly1305_decrypt( + key: &[u8], + nonce: &[u8], + ciphertext_and_tag: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + if ciphertext_and_tag.len() < TAG_LEN { + return Err(CryptoError::InvalidInput( + "ChaCha20-Poly1305: ciphertext too short".into() + )); + } + let ct_len = ciphertext_and_tag.len() - TAG_LEN; + let (ciphertext, tag) = ciphertext_and_tag.split_at(ct_len); + let mut plaintext = vec![0u8; ct_len]; + + unsafe { + // wc_ChaCha20Poly1305_Decrypt(key, nonce, aad, aadSz, input, inputSz, authTag, output) + let ret = crate::sys::wc_ChaCha20Poly1305_Decrypt( + key.as_ptr(), + nonce.as_ptr(), + if aad.is_empty() { std::ptr::null() } else { aad.as_ptr() }, + aad.len() as u32, + ciphertext.as_ptr(), + ct_len as u32, + tag.as_ptr(), + plaintext.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::AuthenticationFailed); + } + } + + Ok(plaintext) +} + +// ────────────────────────────────────────────────────────────────────────────── +// XChaCha20-Poly1305 (24-byte nonce) +// ────────────────────────────────────────────────────────────────────────────── +// +// wolfCrypt does not expose a single XChaCha20-Poly1305 one-shot function. +// We derive a sub-key from the first 16 bytes of the nonce using HChaCha20, +// then apply ChaCha20-Poly1305 with the last 12 bytes for the nonce. +// This matches the XChaCha20-Poly1305 construction in RFC 8439 §2. + +fn xchacha20_poly1305_encrypt( + key: &[u8], + nonce: &[u8], // 24 bytes + plaintext: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + let (subkey, chacha_nonce) = xchacha_derive_subkey(key, nonce)?; + chacha20_poly1305_encrypt(&subkey, &chacha_nonce, plaintext, aad) +} + +fn xchacha20_poly1305_decrypt( + key: &[u8], + nonce: &[u8], // 24 bytes + ciphertext_and_tag: &[u8], + aad: &[u8], +) -> Result, CryptoError> { + let (subkey, chacha_nonce) = xchacha_derive_subkey(key, nonce)?; + chacha20_poly1305_decrypt(&subkey, &chacha_nonce, ciphertext_and_tag, aad) +} + +/// Derive a 32-byte sub-key and a 12-byte ChaCha nonce from a 32-byte key +/// and a 24-byte XChaCha nonce, following RFC 8439 §2. +fn xchacha_derive_subkey(key: &[u8], nonce: &[u8]) -> Result<(Vec, Vec), CryptoError> { + debug_assert_eq!(nonce.len(), 24); + // HChaCha20 takes key[32] + nonce[0..16] → 32-byte subkey. + let mut subkey = vec![0u8; 32]; + unsafe { + // wc_HChaCha20(output, key, nonce_16bytes) — available in wolfCrypt. + let ret = crate::sys::wc_HChaCha20( + subkey.as_mut_ptr() as *mut u32, + key.as_ptr() as *const u32, + nonce.as_ptr() as *const u32, + ); + if ret != 0 { + return Err(CryptoError::InternalError( + format!("wc_HChaCha20 returned {}", ret) + )); + } + } + // ChaCha nonce = [0u8 × 4] || nonce[16..24] + let mut chacha_nonce = vec![0u8; 12]; + chacha_nonce[4..12].copy_from_slice(&nonce[16..24]); + + Ok((subkey, chacha_nonce)) +} diff --git a/crates/ccc-crypto-wolfssl/src/capabilities.rs b/crates/ccc-crypto-wolfssl/src/capabilities.rs new file mode 100644 index 0000000..f2b9ed0 --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/capabilities.rs @@ -0,0 +1,200 @@ +//! Runtime capability probing and benchmarking for wolfSSL. +//! +//! [`probe_capabilities`] runs a one-byte encrypt/decrypt through each +//! algorithm at startup and flips `available` based on whether it succeeds. +//! +//! [`run_benchmark`] runs 100 × 1 MB encryptions per AEAD algorithm to +//! produce throughput scores. Results are stored in the returned +//! [`BenchmarkReport`] and normalised to a 0–100 `efficiency_score`. + +use std::time::Instant; + +use ccc_crypto_core::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}, + capabilities::{AlgorithmCapability, ProviderCapabilities}, + types::{AlgoBenchResult, BenchmarkReport}, +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Capability probing +// ────────────────────────────────────────────────────────────────────────────── + +/// Probe every algorithm by performing a minimal one-shot operation. +/// +/// Sets `available = true` only when the wolfCrypt call returns `0` (success). +/// Sets `efficiency_score` / `reliability_score` from `benchmarks` results +/// when provided, otherwise leaves them at 0 (to be populated later). +pub fn probe_capabilities(benchmarks: Option<&BenchmarkReport>) -> ProviderCapabilities { + let mut caps = ProviderCapabilities::empty("wolfssl"); + + // ── AEAD ───────────────────────────────────────────────────────────────── + for algo in [ + AeadAlgorithm::AesGcm256, + AeadAlgorithm::ChaCha20Poly1305, + AeadAlgorithm::XChaCha20Poly1305, + ] { + let nonce_len = algo.nonce_len(); + let key = vec![0u8; algo.key_len()]; + let nonce = vec![0u8; nonce_len]; + let available = crate::aead::encrypt(algo, &key, &nonce, b"x", b"").is_ok(); + + let efficiency = benchmarks + .and_then(|b| b.results.iter().find(|r| r.algo_id == algo as u32)) + .map(|r| r.efficiency_score) + .unwrap_or(0); + + caps = caps.with_aead(algo, AlgorithmCapability { + available, + deterministic_io: true, // wolfSSL output is deterministic for same key/nonce + efficiency_score: efficiency, + reliability_score: if available { 100 } else { 0 }, + }); + } + + // Ascon-128a is not in this wolfSSL build. + caps = caps.with_aead(AeadAlgorithm::Ascon128a, AlgorithmCapability::unavailable()); + + // ── KDF ────────────────────────────────────────────────────────────────── + for algo in [KdfAlgorithm::Sha256, KdfAlgorithm::Sha384, KdfAlgorithm::Sha512] { + let available = crate::kdf::derive_key(algo, b"key", b"salt", b"info", 32).is_ok(); + caps = caps.with_kdf(algo, AlgorithmCapability { + available, + deterministic_io: true, + efficiency_score: 80, // HKDF is fast; benchmark not run for KDF + reliability_score: if available { 100 } else { 0 }, + }); + } + for algo in [KdfAlgorithm::Blake2b512, KdfAlgorithm::Argon2id] { + let salt: &[u8] = if algo == KdfAlgorithm::Argon2id { b"saltsalt" } else { b"" }; + let available = crate::kdf::derive_key(algo, b"key", salt, b"", 32).is_ok(); + caps = caps.with_kdf(algo, AlgorithmCapability { + available, + deterministic_io: true, + efficiency_score: if algo == KdfAlgorithm::Argon2id { 10 } else { 70 }, + reliability_score: if available { 100 } else { 0 }, + }); + } + caps = caps.with_kdf(KdfAlgorithm::Kmac256, AlgorithmCapability::unavailable()); + + // ── MAC ────────────────────────────────────────────────────────────────── + for algo in [MacAlgorithm::HmacSha256, MacAlgorithm::HmacSha384, MacAlgorithm::HmacSha512] { + let available = crate::mac::compute_mac(algo, b"key", b"data").is_ok(); + caps = caps.with_mac(algo, AlgorithmCapability { + available, + deterministic_io: true, + efficiency_score: 85, + reliability_score: if available { 100 } else { 0 }, + }); + } + { + let available = crate::mac::compute_mac(MacAlgorithm::Blake2bMac, b"key-that-is-16-by", b"data").is_ok(); + caps = caps.with_mac(MacAlgorithm::Blake2bMac, AlgorithmCapability { + available, + deterministic_io: true, + efficiency_score: 90, + reliability_score: if available { 100 } else { 0 }, + }); + } + caps = caps.with_mac(MacAlgorithm::Poly1305, AlgorithmCapability::unavailable()); + + // ── Hash ───────────────────────────────────────────────────────────────── + for algo in [ + HashAlgorithm::Sha256, HashAlgorithm::Sha384, HashAlgorithm::Sha512, + HashAlgorithm::Blake2b512, HashAlgorithm::Sha3_256, HashAlgorithm::Sha3_512, + ] { + let available = crate::hash::hash(algo, b"probe").is_ok(); + caps = caps.with_hash(algo, AlgorithmCapability { + available, + deterministic_io: true, + efficiency_score: 90, + reliability_score: if available { 100 } else { 0 }, + }); + } + + // ── KEM ────────────────────────────────────────────────────────────────── + for algo in [KemAlgorithm::X25519, KemAlgorithm::X448] { + let available = crate::kem::generate_keypair(algo).is_ok(); + caps = caps.with_kem(algo, AlgorithmCapability { + available, + deterministic_io: false, // key generation is randomised + efficiency_score: 80, + reliability_score: if available { 100 } else { 0 }, + }); + } + for algo in [ + KemAlgorithm::MlKem768, KemAlgorithm::MlKem1024, + KemAlgorithm::ClassicMcEliece460896, + ] { + caps = caps.with_kem(algo, AlgorithmCapability::unavailable()); + } + + caps +} + +// ────────────────────────────────────────────────────────────────────────────── +// Throughput benchmarking +// ────────────────────────────────────────────────────────────────────────────── + +/// Run 100 × 1 MB AEAD encryptions for each available algorithm. +/// +/// Returns a [`BenchmarkReport`] with raw throughput values and normalised +/// `efficiency_score` (0–100) relative to the fastest algorithm measured. +pub fn run_benchmark() -> BenchmarkReport { + const MB: usize = 1024 * 1024; + const ITERS: u32 = 100; + let plaintext = vec![0xABu8; MB]; + + let algos = [ + AeadAlgorithm::AesGcm256, + AeadAlgorithm::ChaCha20Poly1305, + AeadAlgorithm::XChaCha20Poly1305, + ]; + + let mut raw_results: Vec<(AeadAlgorithm, f64)> = Vec::new(); + + for algo in algos { + let key = vec![0u8; algo.key_len()]; + let nonce = vec![0u8; algo.nonce_len()]; + + // Probe first; skip unavailable algorithms. + if crate::aead::encrypt(algo, &key, &nonce, b"x", b"").is_err() { + continue; + } + + let start = Instant::now(); + for _ in 0..ITERS { + let _ = crate::aead::encrypt(algo, &key, &nonce, &plaintext, b""); + } + let elapsed_secs = start.elapsed().as_secs_f64(); + let throughput = (MB as f64 * ITERS as f64) / (1024.0 * 1024.0 * elapsed_secs); + raw_results.push((algo, throughput)); + } + + // Normalise to 0–100 relative to the fastest result. + let max_throughput = raw_results + .iter() + .map(|(_, t)| *t) + .fold(f64::NEG_INFINITY, f64::max); + + let results: Vec = raw_results + .into_iter() + .map(|(algo, throughput)| { + let score = if max_throughput > 0.0 { + ((throughput / max_throughput) * 100.0).round() as u8 + } else { + 0 + }; + AlgoBenchResult { + algo_id: algo as u32, + algo_name: algo.name().into(), + throughput_mbps: throughput, + efficiency_score: score, + } + }) + .collect(); + + BenchmarkReport { + provider_name: "wolfssl".into(), + results, + } +} diff --git a/crates/ccc-crypto-wolfssl/src/hash.rs b/crates/ccc-crypto-wolfssl/src/hash.rs new file mode 100644 index 0000000..4754c5e --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/hash.rs @@ -0,0 +1,135 @@ +//! Cryptographic hash implementations via wolfCrypt. +//! +//! Covers SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-512, and BLAKE2b-512. + +use ccc_crypto_core::{algorithms::HashAlgorithm, error::CryptoError}; + +// ────────────────────────────────────────────────────────────────────────────── +// Public entry point +// ────────────────────────────────────────────────────────────────────────────── + +/// Compute a digest of `data` using the specified hash algorithm. +/// +/// Returns the full digest for the selected algorithm (see +/// [`HashAlgorithm::digest_len`] for output lengths). +pub fn hash(algo: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + match algo { + HashAlgorithm::Sha256 => sha256(data), + HashAlgorithm::Sha384 => sha384(data), + HashAlgorithm::Sha512 => sha512(data), + HashAlgorithm::Blake2b512 => blake2b_512(data), + HashAlgorithm::Sha3_256 => sha3_256(data), + HashAlgorithm::Sha3_512 => sha3_512(data), + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// SHA-2 family +// ────────────────────────────────────────────────────────────────────────────── + +fn sha256(data: &[u8]) -> Result, CryptoError> { + let mut out = vec![0u8; 32]; + unsafe { + // wolfCrypt: wc_Sha256Hash(data, dataSz, digest) + let ret = crate::sys::wc_Sha256Hash( + data.as_ptr(), + data.len() as u32, + out.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_Sha256Hash returned {}", ret))); + } + } + Ok(out) +} + +fn sha384(data: &[u8]) -> Result, CryptoError> { + let mut out = vec![0u8; 48]; + unsafe { + let ret = crate::sys::wc_Sha384Hash( + data.as_ptr(), + data.len() as u32, + out.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_Sha384Hash returned {}", ret))); + } + } + Ok(out) +} + +fn sha512(data: &[u8]) -> Result, CryptoError> { + let mut out = vec![0u8; 64]; + unsafe { + let ret = crate::sys::wc_Sha512Hash( + data.as_ptr(), + data.len() as u32, + out.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_Sha512Hash returned {}", ret))); + } + } + Ok(out) +} + +// ────────────────────────────────────────────────────────────────────────────── +// SHA-3 family +// ────────────────────────────────────────────────────────────────────────────── + +fn sha3_256(data: &[u8]) -> Result, CryptoError> { + let mut out = vec![0u8; 32]; + unsafe { + // wolfCrypt: wc_Sha3_256Hash(data, dataSz, digest) + let ret = crate::sys::wc_Sha3_256Hash( + data.as_ptr(), + data.len() as u32, + out.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_Sha3_256Hash returned {}", ret))); + } + } + Ok(out) +} + +fn sha3_512(data: &[u8]) -> Result, CryptoError> { + let mut out = vec![0u8; 64]; + unsafe { + let ret = crate::sys::wc_Sha3_512Hash( + data.as_ptr(), + data.len() as u32, + out.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_Sha3_512Hash returned {}", ret))); + } + } + Ok(out) +} + +// ────────────────────────────────────────────────────────────────────────────── +// BLAKE2b-512 +// ────────────────────────────────────────────────────────────────────────────── + +fn blake2b_512(data: &[u8]) -> Result, CryptoError> { + let mut out = vec![0u8; 64]; + unsafe { + // wc_Blake2bHash(out, outLen, key, keyLen, data, dataSz) + // No key = unkeyed hash (pass NULL, 0). + let ret = crate::sys::wc_Blake2bHash( + out.as_mut_ptr(), + 64u32, + std::ptr::null(), + 0u32, + data.as_ptr(), + data.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InternalError( + format!("wc_Blake2bHash returned {}", ret) + )); + } + } + Ok(out) +} diff --git a/crates/ccc-crypto-wolfssl/src/kdf.rs b/crates/ccc-crypto-wolfssl/src/kdf.rs new file mode 100644 index 0000000..3f64704 --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/kdf.rs @@ -0,0 +1,167 @@ +//! Key derivation function implementations via wolfCrypt. +//! +//! Covers HKDF (SHA-256 / SHA-384 / SHA-512), Argon2id, and BLAKE2b-based KDF. + +use zeroize::Zeroizing; + +use ccc_crypto_core::{algorithms::KdfAlgorithm, error::CryptoError}; + +// ────────────────────────────────────────────────────────────────────────────── +// wolfCrypt hash type constants (passed to wc_HKDF) +// ────────────────────────────────────────────────────────────────────────────── + +/// wolfCrypt `wc_HashType` values for hash algorithms used in HKDF. +/// These match the `enum wc_HashType` values in `wolfssl/wolfcrypt/hash.h`. +const WC_HASH_TYPE_SHA256: i32 = 8; +const WC_HASH_TYPE_SHA384: i32 = 9; +const WC_HASH_TYPE_SHA512: i32 = 10; + +// ────────────────────────────────────────────────────────────────────────────── +// Public entry point +// ────────────────────────────────────────────────────────────────────────────── + +/// Derive a key of `length` bytes from input keying material. +pub fn derive_key( + algo: KdfAlgorithm, + ikm: &[u8], + salt: &[u8], + info: &[u8], + length: usize, +) -> Result>, CryptoError> { + match algo { + KdfAlgorithm::Sha256 => hkdf(WC_HASH_TYPE_SHA256, ikm, salt, info, length), + KdfAlgorithm::Sha384 => hkdf(WC_HASH_TYPE_SHA384, ikm, salt, info, length), + KdfAlgorithm::Sha512 => hkdf(WC_HASH_TYPE_SHA512, ikm, salt, info, length), + KdfAlgorithm::Blake2b512 => blake2b_kdf(ikm, salt, info, length), + KdfAlgorithm::Argon2id => argon2id(ikm, salt, length), + KdfAlgorithm::Kmac256 => + Err(CryptoError::FeatureNotCompiled("KMAC256 (Phase 5+)".into())), + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// HKDF (RFC 5869) +// ────────────────────────────────────────────────────────────────────────────── + +/// HKDF using wolfCrypt's `wc_HKDF()`. +/// +/// `wc_HKDF(type, ikm, ikmSz, salt, saltSz, info, infoSz, okm, okmSz)` +fn hkdf( + hash_type: i32, + ikm: &[u8], + salt: &[u8], + info: &[u8], + length: usize, +) -> Result>, CryptoError> { + let mut out = Zeroizing::new(vec![0u8; length]); + + unsafe { + let ret = crate::sys::wc_HKDF( + hash_type, + ikm.as_ptr(), + ikm.len() as u32, + if salt.is_empty() { std::ptr::null() } else { salt.as_ptr() }, + salt.len() as u32, + if info.is_empty() { std::ptr::null() } else { info.as_ptr() }, + info.len() as u32, + out.as_mut_ptr(), + length as u32, + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_HKDF returned {}", ret))); + } + } + + Ok(out) +} + +// ────────────────────────────────────────────────────────────────────────────── +// BLAKE2b-based KDF +// ────────────────────────────────────────────────────────────────────────────── + +/// BLAKE2b-512 used as a KDF: `BLAKE2b(ikm || salt || info)` → `length` bytes. +/// +/// This is the same construction the Dart `RatchetEngine._kdf()` uses when +/// `kdfFunction == blake2b512`. Output is truncated to `length` bytes. +fn blake2b_kdf( + ikm: &[u8], + salt: &[u8], + info: &[u8], + length: usize, +) -> Result>, CryptoError> { + use crate::hash; + use ccc_crypto_core::algorithms::HashAlgorithm; + + // Concatenate ikm || salt || info and hash the result. + let mut input = Vec::with_capacity(ikm.len() + salt.len() + info.len()); + input.extend_from_slice(ikm); + input.extend_from_slice(salt); + input.extend_from_slice(info); + + let digest = hash::hash(HashAlgorithm::Blake2b512, &input)?; + + // Truncate or zero-extend to the requested length. + // For lengths > 64 (BLAKE2b-512 output) this KDF is not appropriate — + // callers should use HKDF-SHA512 instead. + if length > digest.len() { + return Err(CryptoError::InvalidInput( + "BLAKE2b-512 KDF: requested length exceeds 64 bytes; use HKDF-SHA512".into() + )); + } + + let mut out = Zeroizing::new(vec![0u8; length]); + out.copy_from_slice(&digest[..length]); + Ok(out) +} + +// ────────────────────────────────────────────────────────────────────────────── +// Argon2id +// ────────────────────────────────────────────────────────────────────────────── + +/// Argon2id KDF via wolfCrypt's `wc_Argon2()`. +/// +/// Uses the memory and iteration parameters from `DEFAULT_CIPHER_PARAMS` in +/// `cipher_constants.dart`: 64 MB memory, 4 threads, 3 iterations. +fn argon2id( + password: &[u8], // treated as ikm / password + salt: &[u8], + length: usize, +) -> Result>, CryptoError> { + // Argon2id requires at least 8 bytes of salt. + if salt.len() < 8 { + return Err(CryptoError::InvalidInput( + "Argon2id requires at least 8-byte salt".into() + )); + } + + let mut out = Zeroizing::new(vec![0u8; length]); + + unsafe { + // wolfCrypt Argon2: + // typedef struct Argon2_t { ... } Argon2; + // int wc_Argon2Hash(Argon2* arg2, const byte* pwd, word32 pwdSz, + // const byte* salt, word32 saltSz, byte* out, word32 outSz) + // + // We use the simplified wc_Argon2id_Hash wrapper (available in wolfCrypt ≥ 5.5). + let ret = crate::sys::wc_Argon2id_Hash( + out.as_mut_ptr(), + length as u32, + password.as_ptr(), + password.len() as u32, + salt.as_ptr(), + salt.len() as u32, + std::ptr::null(), // secret + 0u32, // secretLen + std::ptr::null(), // additional data + 0u32, // additional data len + 64 * 1024, // memory kb (64 MB, matches DEFAULT_CIPHER_PARAMS) + 3u32, // iterations (matches DEFAULT_CIPHER_PARAMS) + 4u32, // parallelism (matches DEFAULT_CIPHER_PARAMS: 4 cores) + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_Argon2id_Hash returned {}", ret))); + } + } + + Ok(out) +} diff --git a/crates/ccc-crypto-wolfssl/src/kem.rs b/crates/ccc-crypto-wolfssl/src/kem.rs new file mode 100644 index 0000000..bf332d5 --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/kem.rs @@ -0,0 +1,313 @@ +//! Key Encapsulation Mechanism (KEM) implementations via wolfCrypt. +//! +//! Phase 4 covers X25519 and X448 (classical ECDH-based KEMs). +//! ML-KEM-768/1024 are stubbed with a descriptive error — they will be wired +//! in Phase 5 once the wolfSSL PQ build is validated. + +use zeroize::Zeroizing; + +use ccc_crypto_core::{ + algorithms::KemAlgorithm, + error::CryptoError, + types::{KemEncapResult, KemKeyPair}, +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Public entry points +// ────────────────────────────────────────────────────────────────────────────── + +/// Generate a new KEM key pair using the specified algorithm. +pub fn generate_keypair(algo: KemAlgorithm) -> Result { + match algo { + KemAlgorithm::X25519 => x25519_generate(), + KemAlgorithm::X448 => x448_generate(), + KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 => + Err(CryptoError::FeatureNotCompiled( + "ML-KEM is deferred to Phase 5 (requires wolfSSL PQ build)".into() + )), + KemAlgorithm::ClassicMcEliece460896 => + Err(CryptoError::FeatureNotCompiled( + "Classic-McEliece is deferred to Phase 5".into() + )), + } +} + +/// Encapsulate a shared secret for the holder of `public_key`. +/// +/// For X25519 / X448 this performs an ephemeral DH exchange: +/// - Generate a fresh ephemeral key pair. +/// - Compute DH shared secret with the recipient's public key. +/// - Return `(ephemeral_public_key, shared_secret)`. +pub fn encapsulate( + algo: KemAlgorithm, + recipient_public_key: &[u8], +) -> Result { + match algo { + KemAlgorithm::X25519 => x25519_encapsulate(recipient_public_key), + KemAlgorithm::X448 => x448_encapsulate(recipient_public_key), + KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 => + Err(CryptoError::FeatureNotCompiled( + "ML-KEM is deferred to Phase 5".into() + )), + KemAlgorithm::ClassicMcEliece460896 => + Err(CryptoError::FeatureNotCompiled( + "Classic-McEliece is deferred to Phase 5".into() + )), + } +} + +/// Decapsulate a shared secret from `ciphertext` (the peer's ephemeral public key) +/// using our `private_key`. +pub fn decapsulate( + algo: KemAlgorithm, + private_key: &[u8], + peer_ephemeral_public_key: &[u8], +) -> Result>, CryptoError> { + match algo { + KemAlgorithm::X25519 => x25519_decapsulate(private_key, peer_ephemeral_public_key), + KemAlgorithm::X448 => x448_decapsulate(private_key, peer_ephemeral_public_key), + KemAlgorithm::MlKem768 | KemAlgorithm::MlKem1024 => + Err(CryptoError::FeatureNotCompiled( + "ML-KEM is deferred to Phase 5".into() + )), + KemAlgorithm::ClassicMcEliece460896 => + Err(CryptoError::FeatureNotCompiled( + "Classic-McEliece is deferred to Phase 5".into() + )), + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// X25519 +// wolfCrypt API: wc_curve25519_* (wolfssl/wolfcrypt/curve25519.h) +// Key sizes: private = 32 bytes, public = 32 bytes, shared secret = 32 bytes. +// ────────────────────────────────────────────────────────────────────────────── + +fn x25519_generate() -> Result { + let mut public_key = vec![0u8; 32]; + let mut private_key = vec![0u8; 32]; + + unsafe { + let mut key: crate::sys::curve25519_key = std::mem::zeroed(); + let mut rng: crate::sys::WC_RNG = std::mem::zeroed(); + + let ret = crate::sys::wc_InitRng(&mut rng); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_InitRng returned {}", ret))); + } + + let ret = crate::sys::wc_curve25519_make_key(&mut rng, 32, &mut key); + if ret != 0 { + crate::sys::wc_FreeRng(&mut rng); + return Err(CryptoError::InternalError( + format!("wc_curve25519_make_key returned {}", ret) + )); + } + + let mut pub_sz = public_key.len() as u32; + let mut priv_sz = private_key.len() as u32; + + crate::sys::wc_curve25519_export_key_raw( + &mut key, + private_key.as_mut_ptr(), &mut priv_sz, + public_key.as_mut_ptr(), &mut pub_sz, + ); + + crate::sys::wc_curve25519_free(&mut key); + crate::sys::wc_FreeRng(&mut rng); + } + + Ok(KemKeyPair { public_key, private_key }) +} + +fn x25519_encapsulate(recipient_pub: &[u8]) -> Result { + // Generate a fresh ephemeral key pair. + let ephemeral = x25519_generate()?; + // Compute DH shared secret: ephemeral_private × recipient_public. + let shared = x25519_dh(&ephemeral.private_key, recipient_pub)?; + Ok(KemEncapResult { + // KemKeyPair implements ZeroizeOnDrop (adds Drop) so fields must be cloned. + ciphertext: ephemeral.public_key.clone(), + shared_secret: (*shared).clone(), + }) +} + +fn x25519_decapsulate( + private_key: &[u8], + peer_ephemeral_pub: &[u8], +) -> Result>, CryptoError> { + x25519_dh(private_key, peer_ephemeral_pub) +} + +/// Raw X25519 Diffie-Hellman: `shared = scalar_mult(private_key, public_key)`. +fn x25519_dh(private_key: &[u8], public_key: &[u8]) -> Result>, CryptoError> { + if private_key.len() != 32 || public_key.len() != 32 { + return Err(CryptoError::InvalidKey( + "X25519: keys must be 32 bytes each".into() + )); + } + + let mut shared = Zeroizing::new(vec![0u8; 32]); + + unsafe { + let mut local_key: crate::sys::curve25519_key = std::mem::zeroed(); + let mut remote_key: crate::sys::curve25519_key = std::mem::zeroed(); + + // Import private key. + let ret = crate::sys::wc_curve25519_import_private( + private_key.as_ptr(), 32, + &mut local_key, + ); + if ret != 0 { + return Err(CryptoError::InvalidKey( + format!("wc_curve25519_import_private returned {}", ret) + )); + } + + // Import remote public key. + let ret = crate::sys::wc_curve25519_import_public( + public_key.as_ptr(), 32, + &mut remote_key, + ); + if ret != 0 { + crate::sys::wc_curve25519_free(&mut local_key); + return Err(CryptoError::InvalidKey( + format!("wc_curve25519_import_public returned {}", ret) + )); + } + + let mut shared_sz = 32u32; + // EC_VALUE_SAME_KEY = 1 (little-endian) for Curve25519 in wolfCrypt + let endian = 1i32; + let ret = crate::sys::wc_curve25519_shared_secret_ex( + &mut local_key, + &mut remote_key, + shared.as_mut_ptr(), + &mut shared_sz, + endian, + ); + + crate::sys::wc_curve25519_free(&mut local_key); + crate::sys::wc_curve25519_free(&mut remote_key); + + if ret != 0 { + return Err(CryptoError::InternalError( + format!("wc_curve25519_shared_secret_ex returned {}", ret) + )); + } + } + + Ok(shared) +} + +// ────────────────────────────────────────────────────────────────────────────── +// X448 +// wolfCrypt API: wc_curve448_* (wolfssl/wolfcrypt/curve448.h) +// Key sizes: private = 56 bytes, public = 56 bytes, shared secret = 56 bytes. +// ────────────────────────────────────────────────────────────────────────────── + +fn x448_generate() -> Result { + let mut public_key = vec![0u8; 56]; + let mut private_key = vec![0u8; 56]; + + unsafe { + let mut key: crate::sys::curve448_key = std::mem::zeroed(); + let mut rng: crate::sys::WC_RNG = std::mem::zeroed(); + + let ret = crate::sys::wc_InitRng(&mut rng); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_InitRng returned {}", ret))); + } + + let ret = crate::sys::wc_curve448_make_key(&mut rng, 56, &mut key); + if ret != 0 { + crate::sys::wc_FreeRng(&mut rng); + return Err(CryptoError::InternalError( + format!("wc_curve448_make_key returned {}", ret) + )); + } + + let mut pub_sz = public_key.len() as u32; + let mut priv_sz = private_key.len() as u32; + + crate::sys::wc_curve448_export_key_raw( + &mut key, + private_key.as_mut_ptr(), &mut priv_sz, + public_key.as_mut_ptr(), &mut pub_sz, + ); + + crate::sys::wc_curve448_free(&mut key); + crate::sys::wc_FreeRng(&mut rng); + } + + Ok(KemKeyPair { public_key, private_key }) +} + +fn x448_encapsulate(recipient_pub: &[u8]) -> Result { + let ephemeral = x448_generate()?; + let shared = x448_dh(&ephemeral.private_key, recipient_pub)?; + Ok(KemEncapResult { + ciphertext: ephemeral.public_key.clone(), + shared_secret: (*shared).clone(), + }) +} + +fn x448_decapsulate( + private_key: &[u8], + peer_ephemeral_pub: &[u8], +) -> Result>, CryptoError> { + x448_dh(private_key, peer_ephemeral_pub) +} + +fn x448_dh(private_key: &[u8], public_key: &[u8]) -> Result>, CryptoError> { + if private_key.len() != 56 || public_key.len() != 56 { + return Err(CryptoError::InvalidKey("X448: keys must be 56 bytes each".into())); + } + + let mut shared = Zeroizing::new(vec![0u8; 56]); + + unsafe { + let mut local_key: crate::sys::curve448_key = std::mem::zeroed(); + let mut remote_key: crate::sys::curve448_key = std::mem::zeroed(); + + let ret = crate::sys::wc_curve448_import_private( + private_key.as_ptr(), 56, &mut local_key, + ); + if ret != 0 { + return Err(CryptoError::InvalidKey( + format!("wc_curve448_import_private returned {}", ret) + )); + } + + let ret = crate::sys::wc_curve448_import_public( + public_key.as_ptr(), 56, &mut remote_key, + ); + if ret != 0 { + crate::sys::wc_curve448_free(&mut local_key); + return Err(CryptoError::InvalidKey( + format!("wc_curve448_import_public returned {}", ret) + )); + } + + let mut shared_sz = 56u32; + let endian = 1i32; // EC_VALUE_SAME_KEY little-endian + let ret = crate::sys::wc_curve448_shared_secret_ex( + &mut local_key, + &mut remote_key, + shared.as_mut_ptr(), + &mut shared_sz, + endian, + ); + + crate::sys::wc_curve448_free(&mut local_key); + crate::sys::wc_curve448_free(&mut remote_key); + + if ret != 0 { + return Err(CryptoError::InternalError( + format!("wc_curve448_shared_secret_ex returned {}", ret) + )); + } + } + + Ok(shared) +} diff --git a/crates/ccc-crypto-wolfssl/src/lib.rs b/crates/ccc-crypto-wolfssl/src/lib.rs new file mode 100644 index 0000000..393a530 --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/lib.rs @@ -0,0 +1,206 @@ +//! # ccc-crypto-wolfssl +//! +//! wolfSSL / wolfCrypt implementation of the [`ccc_crypto_core::CryptoProvider`] +//! trait. This crate builds wolfSSL from source (vendored git submodule) and +//! wraps the C functions in safe Rust using FFI bindings generated by bindgen. +//! +//! ## Usage +//! +//! Call [`init()`] once at application start (from the bridge crate's +//! `ccc_init()` function). That registers a [`WolfSslProvider`] into the +//! global [`ProviderRegistry`][ccc_crypto_core::registry::ProviderRegistry]. + +pub mod aead; +pub mod capabilities; +pub mod hash; +pub mod kdf; +pub mod kem; +pub mod mac; +pub mod provider; + +// FFI bindings generated by bindgen in build.rs. +// When wolfSSL headers are not present (e.g. docs.rs), use a stub. +#[allow(non_camel_case_types, non_snake_case, non_upper_case_globals, dead_code, clippy::all)] +mod sys { + // In a real build this pulls in the bindgen-generated file. + // The `include!` macro resolves at compile time to the OUT_DIR path. + // We guard it behind a cfg so the module still compiles without headers. + #[cfg(not(feature = "stub_ffi"))] + include!(concat!(env!("OUT_DIR"), "/wolfcrypt_bindings.rs")); + + // Stub module for environments without the wolfSSL C library. + #[cfg(feature = "stub_ffi")] + pub use self::stubs::*; + + #[cfg(feature = "stub_ffi")] + pub mod stubs { + //! Compile-time stubs for all wolfCrypt C types and functions used in + //! this crate. These allow `cargo check --features stub_ffi` to succeed + //! without the wolfSSL shared library or headers being present. + //! + //! **None of these functions will ever be called** — the `stub_ffi` + //! feature is only used for type-checking and documentation builds. + #![allow(non_camel_case_types, dead_code, unused_variables)] + + use std::os::raw::{c_int, c_uchar, c_uint}; + + // ── Opaque C struct stubs ──────────────────────────────────────────── + + /// Stub for wolfCrypt `Aes` struct. + #[repr(C)] + pub struct Aes([u8; 512]); + + /// Stub for wolfCrypt `Hmac` struct. + #[repr(C)] + pub struct Hmac([u8; 512]); + + /// Stub for wolfCrypt `WC_RNG` struct. + #[repr(C)] + pub struct WC_RNG([u8; 256]); + + /// Stub for wolfCrypt `curve25519_key` struct. + #[repr(C)] + pub struct curve25519_key([u8; 256]); + + /// Stub for wolfCrypt `curve448_key` struct. + #[repr(C)] + pub struct curve448_key([u8; 256]); + + // ── AES-GCM ───────────────────────────────────────────────────────── + + pub unsafe fn wc_AesGcmSetKey(aes: *mut Aes, key: *const c_uchar, len: c_uint) -> c_int { unreachable!() } + pub unsafe fn wc_AesGcmEncrypt( + aes: *mut Aes, out: *mut c_uchar, in_: *const c_uchar, sz: c_uint, + iv: *const c_uchar, iv_sz: c_uint, auth_tag: *mut c_uchar, auth_tag_sz: c_uint, + auth_in: *const c_uchar, auth_in_sz: c_uint, + ) -> c_int { unreachable!() } + pub unsafe fn wc_AesGcmDecrypt( + aes: *mut Aes, out: *mut c_uchar, in_: *const c_uchar, sz: c_uint, + iv: *const c_uchar, iv_sz: c_uint, auth_tag: *const c_uchar, auth_tag_sz: c_uint, + auth_in: *const c_uchar, auth_in_sz: c_uint, + ) -> c_int { unreachable!() } + + // ── ChaCha20-Poly1305 ──────────────────────────────────────────────── + + pub unsafe fn wc_ChaCha20Poly1305_Encrypt( + key: *const c_uchar, iv: *const c_uchar, + aad: *const c_uchar, aad_sz: c_uint, + in_: *const c_uchar, in_sz: c_uint, + out: *mut c_uchar, auth_tag: *mut c_uchar, + ) -> c_int { unreachable!() } + pub unsafe fn wc_ChaCha20Poly1305_Decrypt( + key: *const c_uchar, iv: *const c_uchar, + aad: *const c_uchar, aad_sz: c_uint, + in_: *const c_uchar, in_sz: c_uint, + auth_tag: *const c_uchar, out: *mut c_uchar, + ) -> c_int { unreachable!() } + + // ── HChaCha20 (for XChaCha20 sub-key) ─────────────────────────────── + // wc_HChaCha20(out: *mut u32, key: *const u32, nonce_16: *const u32) -> c_int + pub unsafe fn wc_HChaCha20( + out: *mut std::os::raw::c_uint, + key: *const std::os::raw::c_uint, + nonce: *const std::os::raw::c_uint, + ) -> c_int { unreachable!() } + + // ── KDF ───────────────────────────────────────────────────────────── + + pub unsafe fn wc_HKDF( + type_: c_int, + ikm: *const c_uchar, ikm_sz: c_uint, + salt: *const c_uchar, salt_sz: c_uint, + info: *const c_uchar, info_sz: c_uint, + out: *mut c_uchar, out_sz: c_uint, + ) -> c_int { unreachable!() } + + pub unsafe fn wc_Argon2id_Hash( + out: *mut c_uchar, out_sz: c_uint, + pwd: *const c_uchar, pwd_sz: c_uint, + salt: *const c_uchar, salt_sz: c_uint, + secret: *const c_uchar, secret_sz: c_uint, + ad: *const c_uchar, ad_sz: c_uint, + mem_kb: c_uint, iterations: c_uint, parallelism: c_uint, + ) -> c_int { unreachable!() } + + // ── BLAKE2b hash / MAC ─────────────────────────────────────────────── + + pub unsafe fn wc_Blake2bHash( + out: *mut c_uchar, out_sz: c_uint, + in_: *const c_uchar, in_sz: c_uint, + key: *const c_uchar, key_sz: c_uint, // key_sz = 0 for unkeyed + ) -> c_int { unreachable!() } + + // ── HMAC ──────────────────────────────────────────────────────────── + + pub unsafe fn wc_HmacSetKey(hmac: *mut Hmac, type_: c_int, key: *const c_uchar, key_sz: c_uint) -> c_int { unreachable!() } + pub unsafe fn wc_HmacUpdate(hmac: *mut Hmac, in_: *const c_uchar, in_sz: c_uint) -> c_int { unreachable!() } + pub unsafe fn wc_HmacFinal(hmac: *mut Hmac, out: *mut c_uchar) -> c_int { unreachable!() } + pub unsafe fn wc_HmacFree(hmac: *mut Hmac) { unreachable!() } + + // ── One-shot hashes ────────────────────────────────────────────────── + + pub unsafe fn wc_Sha256Hash(in_: *const c_uchar, in_sz: c_uint, out: *mut c_uchar) -> c_int { unreachable!() } + pub unsafe fn wc_Sha384Hash(in_: *const c_uchar, in_sz: c_uint, out: *mut c_uchar) -> c_int { unreachable!() } + pub unsafe fn wc_Sha512Hash(in_: *const c_uchar, in_sz: c_uint, out: *mut c_uchar) -> c_int { unreachable!() } + pub unsafe fn wc_Sha3_256Hash(in_: *const c_uchar, in_sz: c_uint, out: *mut c_uchar) -> c_int { unreachable!() } + pub unsafe fn wc_Sha3_512Hash(in_: *const c_uchar, in_sz: c_uint, out: *mut c_uchar) -> c_int { unreachable!() } + + // ── RNG ───────────────────────────────────────────────────────────── + + pub unsafe fn wc_InitRng(rng: *mut WC_RNG) -> c_int { unreachable!() } + pub unsafe fn wc_FreeRng(rng: *mut WC_RNG) -> c_int { unreachable!() } + + // ── Curve25519 ─────────────────────────────────────────────────────── + + pub unsafe fn wc_curve25519_make_key(rng: *mut WC_RNG, key_sz: c_int, key: *mut curve25519_key) -> c_int { unreachable!() } + pub unsafe fn wc_curve25519_export_key_raw( + key: *mut curve25519_key, + priv_: *mut c_uchar, priv_sz: *mut c_uint, + pub_: *mut c_uchar, pub_sz: *mut c_uint, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve25519_import_private( + priv_: *const c_uchar, priv_sz: c_uint, key: *mut curve25519_key, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve25519_import_public( + pub_: *const c_uchar, pub_sz: c_uint, key: *mut curve25519_key, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve25519_shared_secret_ex( + local: *mut curve25519_key, remote: *mut curve25519_key, + out: *mut c_uchar, out_sz: *mut c_uint, endian: c_int, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve25519_free(key: *mut curve25519_key) { unreachable!() } + + // ── Curve448 ───────────────────────────────────────────────────────── + + pub unsafe fn wc_curve448_make_key(rng: *mut WC_RNG, key_sz: c_int, key: *mut curve448_key) -> c_int { unreachable!() } + pub unsafe fn wc_curve448_export_key_raw( + key: *mut curve448_key, + priv_: *mut c_uchar, priv_sz: *mut c_uint, + pub_: *mut c_uchar, pub_sz: *mut c_uint, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve448_import_private( + priv_: *const c_uchar, priv_sz: c_uint, key: *mut curve448_key, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve448_import_public( + pub_: *const c_uchar, pub_sz: c_uint, key: *mut curve448_key, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve448_shared_secret_ex( + local: *mut curve448_key, remote: *mut curve448_key, + out: *mut c_uchar, out_sz: *mut c_uint, endian: c_int, + ) -> c_int { unreachable!() } + pub unsafe fn wc_curve448_free(key: *mut curve448_key) { unreachable!() } + } +} + +pub use provider::WolfSslProvider; + +/// Register the wolfSSL provider into the global +/// [`ProviderRegistry`][ccc_crypto_core::registry::ProviderRegistry]. +/// +/// Call this once from `ccc_init()` in the bridge crate. +pub fn init() { + let provider = WolfSslProvider::new(); + ccc_crypto_core::ProviderRegistry::global() + .register("wolfssl", Box::new(provider)); + log::info!("[ccc-crypto-wolfssl] provider registered"); +} diff --git a/crates/ccc-crypto-wolfssl/src/mac.rs b/crates/ccc-crypto-wolfssl/src/mac.rs new file mode 100644 index 0000000..e31ea6a --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/mac.rs @@ -0,0 +1,173 @@ +//! MAC (Message Authentication Code) implementations via wolfCrypt. +//! +//! Provides HMAC-SHA256, HMAC-SHA384, HMAC-SHA512, and BLAKE2b-MAC. +//! All `verify_mac` implementations use constant-time comparison to prevent +//! timing side-channels. + +use ccc_crypto_core::{algorithms::MacAlgorithm, error::CryptoError}; + +// wolfCrypt hash type constants matching `enum wc_HashType` in hash.h. +const WC_HASH_TYPE_SHA256: i32 = 8; +const WC_HASH_TYPE_SHA384: i32 = 9; +const WC_HASH_TYPE_SHA512: i32 = 10; + +// ────────────────────────────────────────────────────────────────────────────── +// Public entry points +// ────────────────────────────────────────────────────────────────────────────── + +/// Compute a MAC tag over `data` using the specified algorithm and `key`. +pub fn compute_mac( + algo: MacAlgorithm, + key: &[u8], + data: &[u8], +) -> Result, CryptoError> { + match algo { + MacAlgorithm::HmacSha256 => hmac(WC_HASH_TYPE_SHA256, key, data, 32), + MacAlgorithm::HmacSha384 => hmac(WC_HASH_TYPE_SHA384, key, data, 48), + MacAlgorithm::HmacSha512 => hmac(WC_HASH_TYPE_SHA512, key, data, 64), + MacAlgorithm::Blake2bMac => blake2b_mac(key, data), + MacAlgorithm::Poly1305 => + Err(CryptoError::UnsupportedAlgorithm( + "Poly1305 standalone MAC is not exposed in this Phase".into() + )), + } +} + +/// Verify a MAC tag in constant time. +/// +/// Returns `Ok(true)` if the tag matches, `Ok(false)` if it does not. +/// Never returns `Err` for a tag mismatch — only for structural errors. +pub fn verify_mac( + algo: MacAlgorithm, + key: &[u8], + data: &[u8], + mac: &[u8], +) -> Result { + let expected = compute_mac(algo, key, data)?; + Ok(constant_time_eq(&expected, mac)) +} + +// ────────────────────────────────────────────────────────────────────────────── +// HMAC +// ────────────────────────────────────────────────────────────────────────────── + +/// HMAC using wolfCrypt's `wc_HmacSetKey` / `wc_HmacUpdate` / `wc_HmacFinal`. +fn hmac( + hash_type: i32, + key: &[u8], + data: &[u8], + out_len: usize, +) -> Result, CryptoError> { + let mut out = vec![0u8; out_len]; + + unsafe { + let mut hmac_ctx: crate::sys::Hmac = std::mem::zeroed(); + + let ret = crate::sys::wc_HmacSetKey( + &mut hmac_ctx, + hash_type, + key.as_ptr(), + key.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InvalidKey(format!("wc_HmacSetKey returned {}", ret))); + } + + let ret = crate::sys::wc_HmacUpdate( + &mut hmac_ctx, + data.as_ptr(), + data.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_HmacUpdate returned {}", ret))); + } + + let ret = crate::sys::wc_HmacFinal( + &mut hmac_ctx, + out.as_mut_ptr(), + ); + if ret != 0 { + return Err(CryptoError::InternalError(format!("wc_HmacFinal returned {}", ret))); + } + + crate::sys::wc_HmacFree(&mut hmac_ctx); + } + + Ok(out) +} + +// ────────────────────────────────────────────────────────────────────────────── +// BLAKE2b-MAC (keyed BLAKE2b) +// ────────────────────────────────────────────────────────────────────────────── + +/// Keyed BLAKE2b-512 used as a MAC. +/// +/// Key length must be 1–64 bytes (wolfCrypt limitation). +/// Output is always 64 bytes (BLAKE2b-512 full digest length). +fn blake2b_mac(key: &[u8], data: &[u8]) -> Result, CryptoError> { + if key.is_empty() || key.len() > 64 { + return Err(CryptoError::InvalidKey( + "BLAKE2b-MAC: key must be 1–64 bytes".into() + )); + } + + let mut out = vec![0u8; 64]; + + unsafe { + // wolfCrypt: wc_Blake2b(blake2b, out, outLen, key, keyLen, data, dataSz) + // Using the simplified one-shot Blake2bHash wrapper. + let ret = crate::sys::wc_Blake2bHash( + out.as_mut_ptr(), + 64u32, + key.as_ptr(), + key.len() as u32, + data.as_ptr(), + data.len() as u32, + ); + if ret != 0 { + return Err(CryptoError::InternalError( + format!("wc_Blake2bHash returned {}", ret) + )); + } + } + + Ok(out) +} + +// ────────────────────────────────────────────────────────────────────────────── +// Constant-time comparison +// ────────────────────────────────────────────────────────────────────────────── + +/// Constant-time byte-slice equality comparison. +/// +/// Returns `false` immediately if lengths differ (safe — length is not secret +/// for fixed-size HMAC tags). +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + // Accumulate XOR differences; the branch is on the final accumulated value + // only, not on any individual byte. + let diff: u8 = a.iter().zip(b.iter()).fold(0u8, |acc, (x, y)| acc | (x ^ y)); + diff == 0 +} + +#[cfg(test)] +mod tests { + use super::constant_time_eq; + + #[test] + fn ct_eq_same() { + assert!(constant_time_eq(b"hello", b"hello")); + } + + #[test] + fn ct_eq_different() { + assert!(!constant_time_eq(b"hello", b"Hello")); + } + + #[test] + fn ct_eq_different_lengths() { + assert!(!constant_time_eq(b"hi", b"hello")); + } +} diff --git a/crates/ccc-crypto-wolfssl/src/provider.rs b/crates/ccc-crypto-wolfssl/src/provider.rs new file mode 100644 index 0000000..293285d --- /dev/null +++ b/crates/ccc-crypto-wolfssl/src/provider.rs @@ -0,0 +1,250 @@ +//! [`WolfSslProvider`] — the main provider struct implementing +//! [`CryptoProvider`] for the wolfSSL / wolfCrypt library. +//! +//! Capabilities are live-probed on first construction. Benchmark results +//! feed into the `efficiency_score` fields of [`ProviderCapabilities`]. + +use std::sync::OnceLock; + +use zeroize::Zeroizing; + +use ccc_crypto_core::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}, + capabilities::ProviderCapabilities, + error::CryptoError, + provider::{ + AeadProvider, CryptoProvider, HashProvider, KdfProvider, KemProvider, MacProvider, + }, + types::{AlgoTestResult, BenchmarkReport, KemEncapResult, KemKeyPair, SelfTestReport}, +}; + +use crate::{aead, capabilities, hash, kdf, kem, mac}; + +// ────────────────────────────────────────────────────────────────────────────── +// Embedded NIST / RFC test vectors for self_test() +// These are checked at runtime; failures gate the provider from being marked +// `available` in the capability catalog. +// ────────────────────────────────────────────────────────────────────────────── + +struct AeadVector { + algo: AeadAlgorithm, + key: &'static str, + nonce: &'static str, + aad: &'static str, + pt: &'static str, + ct_tag: &'static str, // ciphertext || tag, hex-encoded +} + +/// NIST SP 800-38D and RFC 8439 test vectors. +static AEAD_VECTORS: &[AeadVector] = &[ + // ── AES-256-GCM: NIST CAVS test case GCM-256/96/128 (test case 1) ─────── + AeadVector { + algo: AeadAlgorithm::AesGcm256, + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + nonce: "cafebabefacedbaddecaf888", + aad: "", + pt: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + ct_tag: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad", + }, + // ── ChaCha20-Poly1305: RFC 8439 §2.8.2 test vector ────────────────────── + AeadVector { + algo: AeadAlgorithm::ChaCha20Poly1305, + key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + nonce: "070000004041424344454647", + aad: "50515253c0c1c2c3c4c5c6c7", + pt: "4c616469657320616e642047656e746c656d656e206f662074686520636c617373\ + 206f6620273939", + ct_tag: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63\ + dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b369\ + 2ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3\ + ff4def08e4b7a9de576d26586cec64b6116", + }, +]; + +// ────────────────────────────────────────────────────────────────────────────── +// WolfSslProvider +// ────────────────────────────────────────────────────────────────────────────── + +/// The wolfSSL / wolfCrypt crypto provider. +/// +/// Constructed by [`crate::init()`] and registered into the global +/// [`ProviderRegistry`][ccc_crypto_core::registry::ProviderRegistry]. +/// +/// Capabilities and benchmark results are computed lazily on first access to +/// avoid slowing down startup if the provider is never queried. +pub struct WolfSslProvider { + /// Lazy-initialised capabilities + benchmark results. + caps: OnceLock, + bench: OnceLock, +} + +impl WolfSslProvider { + /// Create a new wolfSSL provider. + /// + /// Does not run probes or benchmarks at construction time — those are + /// deferred to the first call to [`capabilities()`] / [`benchmark()`]. + pub fn new() -> Self { + Self { + caps: OnceLock::new(), + bench: OnceLock::new(), + } + } + + /// Return the cached benchmark report, running the benchmark if needed. + fn get_or_run_benchmark(&self) -> &BenchmarkReport { + self.bench.get_or_init(|| { + log::info!("[ccc-crypto-wolfssl] running throughput benchmark…"); + let report = capabilities::run_benchmark(); + log::info!("[ccc-crypto-wolfssl] benchmark complete ({} algos)", report.results.len()); + report + }) + } +} + +impl Default for WolfSslProvider { + fn default() -> Self { Self::new() } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Primitive trait implementations — thin delegation to module functions +// ────────────────────────────────────────────────────────────────────────────── + +impl AeadProvider for WolfSslProvider { + fn encrypt_aead( + &self, algo: AeadAlgorithm, key: &[u8], nonce: &[u8], + plaintext: &[u8], aad: &[u8], + ) -> Result, CryptoError> { + aead::encrypt(algo, key, nonce, plaintext, aad) + } + + fn decrypt_aead( + &self, algo: AeadAlgorithm, key: &[u8], nonce: &[u8], + ciphertext_and_tag: &[u8], aad: &[u8], + ) -> Result, CryptoError> { + aead::decrypt(algo, key, nonce, ciphertext_and_tag, aad) + } +} + +impl KdfProvider for WolfSslProvider { + fn derive_key( + &self, algo: KdfAlgorithm, ikm: &[u8], salt: &[u8], info: &[u8], length: usize, + ) -> Result>, CryptoError> { + kdf::derive_key(algo, ikm, salt, info, length) + } +} + +impl MacProvider for WolfSslProvider { + fn compute_mac( + &self, algo: MacAlgorithm, key: &[u8], data: &[u8], + ) -> Result, CryptoError> { + mac::compute_mac(algo, key, data) + } + + fn verify_mac( + &self, algo: MacAlgorithm, key: &[u8], data: &[u8], mac_bytes: &[u8], + ) -> Result { + mac::verify_mac(algo, key, data, mac_bytes) + } +} + +impl HashProvider for WolfSslProvider { + fn hash(&self, algo: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + hash::hash(algo, data) + } +} + +/// `WolfSslProvider` also implements `KemProvider` but it is not part of the +/// `CryptoProvider` supertrait. Bridge functions call it directly. +impl KemProvider for WolfSslProvider { + fn generate_keypair(&self, algo: KemAlgorithm) -> Result { + kem::generate_keypair(algo) + } + fn encapsulate( + &self, algo: KemAlgorithm, public_key: &[u8], + ) -> Result { + kem::encapsulate(algo, public_key) + } + fn decapsulate( + &self, algo: KemAlgorithm, private_key: &[u8], ciphertext: &[u8], + ) -> Result>, CryptoError> { + kem::decapsulate(algo, private_key, ciphertext) + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// CryptoProvider umbrella impl +// ────────────────────────────────────────────────────────────────────────────── + +impl CryptoProvider for WolfSslProvider { + fn provider_name(&self) -> &'static str { "wolfssl" } + + fn capabilities(&self) -> ProviderCapabilities { + self.caps.get_or_init(|| { + let bench = self.get_or_run_benchmark(); + capabilities::probe_capabilities(Some(bench)) + }).clone() + } + + fn self_test(&self) -> SelfTestReport { + let mut results: Vec = Vec::new(); + + // Run AEAD test vectors. + for v in AEAD_VECTORS { + let result = run_aead_vector(v); + results.push(result); + } + + SelfTestReport::finalise("wolfssl", results) + } + + fn benchmark(&self) -> BenchmarkReport { + self.get_or_run_benchmark().clone() + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Test vector runner +// ────────────────────────────────────────────────────────────────────────────── + +fn run_aead_vector(v: &AeadVector) -> AlgoTestResult { + let test_name = format!("{} NIST/RFC vector", v.algo.name()); + + let decode = |hex: &str| -> Vec { + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap_or(0)) + .collect() + }; + + let key = decode(v.key); + let nonce = decode(v.nonce); + let aad = decode(v.aad); + let pt = decode(v.pt); + let expected = decode(v.ct_tag); + + match aead::encrypt(v.algo, &key, &nonce, &pt, &aad) { + Ok(ct_tag) if ct_tag == expected => AlgoTestResult { + algo_id: v.algo as u32, + algo_name: test_name, + passed: true, + error_message: None, + }, + Ok(ct_tag) => AlgoTestResult { + algo_id: v.algo as u32, + algo_name: test_name.clone(), + passed: false, + error_message: Some(format!( + "output mismatch: got {} bytes, expected {} bytes", + ct_tag.len(), expected.len() + )), + }, + Err(e) => AlgoTestResult { + algo_id: v.algo as u32, + algo_name: test_name, + passed: false, + error_message: Some(e.to_string()), + }, + } +} diff --git a/crates/ccc-flutter-bridge/Cargo.toml b/crates/ccc-flutter-bridge/Cargo.toml new file mode 100644 index 0000000..be12d21 --- /dev/null +++ b/crates/ccc-flutter-bridge/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ccc-flutter-bridge" +version = { workspace = true } +edition = { workspace = true } +authors = { workspace = true } +description = "flutter_rust_bridge v2 entry-point exposing CCC Rust crypto to Dart" + +# Both output kinds are required: +# - staticlib → iOS (linked into XCFramework / Runner) +# - cdylib → macOS / Android (.dylib / .so) +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +ccc-crypto-core = { path = "../ccc-crypto-core" } +ccc-crypto-wolfssl = { path = "../ccc-crypto-wolfssl" } + +flutter_rust_bridge = { version = "=2.9.0" } +zeroize = { workspace = true } +log = { workspace = true } +anyhow = { workspace = true } +env_logger = { workspace = true } + +# Convenience hex encoding for test / debug bridge calls. +hex = "0.4" + +[dev-dependencies] +# Nothing needed; actual tests live in tests/conformance/. diff --git a/crates/ccc-flutter-bridge/src/bridge.rs b/crates/ccc-flutter-bridge/src/bridge.rs new file mode 100644 index 0000000..b3761b2 --- /dev/null +++ b/crates/ccc-flutter-bridge/src/bridge.rs @@ -0,0 +1,279 @@ +//! Public bridge functions — annotated with `#[frb(sync)]` or `#[frb]` so +//! that `flutter_rust_bridge` code-generation emits matching Dart APIs. +//! +//! All fallible functions return `anyhow::Result` (FRB v2 maps that to a +//! Dart `Future` that throws on error). +//! +//! # Naming convention +//! Functions are named `ccc__` so the generated Dart class +//! is pleasingly namespaced as `CccBridge.xxxYyy(…)`. + +use flutter_rust_bridge::frb; +use zeroize::Zeroizing; + +use ccc_crypto_core::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}, + provider::{AeadProvider, HashProvider, KdfProvider, KemProvider, MacProvider}, + registry::ProviderRegistry, +}; + +use super::dto::{ + AeadEncryptRequest, AeadEncryptResult, AlgorithmCapabilityDto, HashRequest, KdfDeriveRequest, + KemEncapResultDto, KemKeyPairDto, MacComputeRequest, ProviderCapabilitiesDto, + SelfTestReportDto, +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Initialisation +// ────────────────────────────────────────────────────────────────────────────── + +/// Initialise the CCC Rust layer. +/// +/// **Must** be called once before any other bridge function, typically during +/// the app's boot sequence in `main.dart` (or `AppState.init()`). +/// +/// Registers the wolfSSL provider into the global [`ProviderRegistry`]. +/// Subsequent calls are no-ops. +#[frb(sync)] +pub fn ccc_init() { + static ONCE: std::sync::OnceLock<()> = std::sync::OnceLock::new(); + ONCE.get_or_init(|| { + let _ = env_logger::try_init(); // tolerate error when already initialised + ccc_crypto_wolfssl::init(); + log::info!("[ccc-bridge] wolfSSL provider registered"); + }); +} + +// ────────────────────────────────────────────────────────────────────────────── +// Provider registry queries +// ────────────────────────────────────────────────────────────────────────────── + +/// List all registered provider names. +#[frb(sync)] +pub fn ccc_list_providers() -> Vec { + ProviderRegistry::global().list() +} + +/// Retrieve full capabilities for a named provider. +/// +/// Returns `None` if the provider is not registered. +#[frb(sync)] +pub fn ccc_capabilities(provider_name: String) -> Option { + let reg = ProviderRegistry::global(); + let guard = reg.get(&provider_name)?; + Some(guard.capabilities().into()) +} + +/// List all algorithms of a specific family that are **available** in a provider. +/// +/// `family` is one of `"aead"`, `"kdf"`, `"mac"`, `"hash"`, `"kem"`. +#[frb(sync)] +pub fn ccc_available_algorithms( + provider_name: String, + family: String, +) -> Vec { + let reg = ProviderRegistry::global(); + let Some(guard) = reg.get(&provider_name) else { return vec![] }; + + // Convert the full capability map to DTO (which flattens HashMaps → Vecs). + let caps_dto: ProviderCapabilitiesDto = guard.capabilities().into(); + + let mut list = match family.as_str() { + "aead" => caps_dto.aead, + "kdf" => caps_dto.kdf, + "mac" => caps_dto.mac, + "hash" => caps_dto.hash, + "kem" => caps_dto.kem, + _ => vec![], + }; + + list.retain(|c| c.available); + list +} + +// ────────────────────────────────────────────────────────────────────────────── +// AEAD +// ────────────────────────────────────────────────────────────────────────────── + +/// Encrypt with an AEAD cipher. +/// +/// Returns `ciphertext || 16-byte authentication tag` on success. +/// +/// # Errors +/// Propagates [`ccc_crypto_core::error::CryptoError`] as an `anyhow::Result`. +pub fn ccc_aead_encrypt(req: AeadEncryptRequest) -> anyhow::Result { + let algo = AeadAlgorithm::from_u32(req.algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown AEAD algo_id: {}", req.algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + let ct_tag = guard.encrypt_aead(algo, &req.key, &req.nonce, &req.plaintext, &req.aad)?; + Ok(AeadEncryptResult { ciphertext_and_tag: ct_tag }) +} + +/// Decrypt with an AEAD cipher. +/// +/// Expects `ciphertext || 16-byte authentication tag` as `ciphertext_and_tag`. +/// Returns plaintext on success; returns an error if authentication fails. +pub fn ccc_aead_decrypt( + algo_id: u32, + key: Vec, + nonce: Vec, + ciphertext_and_tag: Vec, + aad: Vec, +) -> anyhow::Result> { + let algo = AeadAlgorithm::from_u32(algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown AEAD algo_id: {}", algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + Ok(guard.decrypt_aead(algo, &key, &nonce, &ciphertext_and_tag, &aad)?) +} + +// ────────────────────────────────────────────────────────────────────────────── +// KDF +// ────────────────────────────────────────────────────────────────────────────── + +/// Derive a key using a KDF. +/// +/// Returns the derived key bytes (length = `req.out_length`). +pub fn ccc_kdf_derive(req: KdfDeriveRequest) -> anyhow::Result> { + let algo = KdfAlgorithm::from_u32(req.algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown KDF algo_id: {}", req.algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + let out: Zeroizing> = + guard.derive_key(algo, &req.ikm, &req.salt, &req.info, req.out_length as usize)?; + + // Dart does not share memory with Rust; returning a plain Vec is safe. + Ok(out.to_vec()) +} + +// ────────────────────────────────────────────────────────────────────────────── +// MAC +// ────────────────────────────────────────────────────────────────────────────── + +/// Compute a MAC tag. +pub fn ccc_mac_compute(req: MacComputeRequest) -> anyhow::Result> { + let algo = MacAlgorithm::from_u32(req.algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown MAC algo_id: {}", req.algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + Ok(guard.compute_mac(algo, &req.key, &req.data)?) +} + +/// Verify a MAC tag in constant time (returns `true` if authentic). +pub fn ccc_mac_verify( + algo_id: u32, + key: Vec, + data: Vec, + mac_bytes: Vec, +) -> anyhow::Result { + let algo = MacAlgorithm::from_u32(algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown MAC algo_id: {}", algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + Ok(guard.verify_mac(algo, &key, &data, &mac_bytes)?) +} + +// ────────────────────────────────────────────────────────────────────────────── +// Hash +// ────────────────────────────────────────────────────────────────────────────── + +/// Compute a cryptographic hash. +pub fn ccc_hash(req: HashRequest) -> anyhow::Result> { + let algo = HashAlgorithm::from_u32(req.algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown Hash algo_id: {}", req.algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + Ok(guard.hash(algo, &req.data)?) +} + +// ────────────────────────────────────────────────────────────────────────────── +// KEM +// ────────────────────────────────────────────────────────────────────────────── + +/// Generate a KEM key pair. +pub fn ccc_kem_generate_keypair(algo_id: u32) -> anyhow::Result { + let algo = KemAlgorithm::from_u32(algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown KEM algo_id: {}", algo_id))?; + + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + // WolfSslProvider also implements KemProvider. + // Down-cast via concrete type via the global init path. + use ccc_crypto_wolfssl::provider::WolfSslProvider; + let _ = guard; // release the Arc lock before constructing a temporary + + let temp = WolfSslProvider::new(); + Ok(temp.generate_keypair(algo)?.into()) +} + +/// Encapsulate a shared secret to a public key. +pub fn ccc_kem_encapsulate(algo_id: u32, public_key: Vec) -> anyhow::Result { + let algo = KemAlgorithm::from_u32(algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown KEM algo_id: {}", algo_id))?; + + use ccc_crypto_wolfssl::provider::WolfSslProvider; + let temp = WolfSslProvider::new(); + Ok(temp.encapsulate(algo, &public_key)?.into()) +} + +/// Decapsulate a shared secret using a private key. +/// +/// Returns the shared secret bytes. +pub fn ccc_kem_decapsulate( + algo_id: u32, + private_key: Vec, + ciphertext: Vec, +) -> anyhow::Result> { + let algo = KemAlgorithm::from_u32(algo_id) + .ok_or_else(|| anyhow::anyhow!("unknown KEM algo_id: {}", algo_id))?; + + use ccc_crypto_wolfssl::provider::WolfSslProvider; + let temp = WolfSslProvider::new(); + let secret: Zeroizing> = temp.decapsulate(algo, &private_key, &ciphertext)?; + Ok(secret.to_vec()) +} + +// ────────────────────────────────────────────────────────────────────────────── +// Self-test +// ────────────────────────────────────────────────────────────────────────────── + +/// Run the embedded NIST / RFC test vectors for the wolfSSL provider. +/// +/// Returns a full [`SelfTestReportDto`]; `all_passed == true` means every +/// algorithm passed its vectors. Check individual `results` for details. +pub fn ccc_self_test() -> anyhow::Result { + let reg = ProviderRegistry::global(); + let guard = reg + .get("wolfssl") + .ok_or_else(|| anyhow::anyhow!("wolfssl provider not registered"))?; + + Ok(guard.self_test().into()) +} diff --git a/crates/ccc-flutter-bridge/src/dto.rs b/crates/ccc-flutter-bridge/src/dto.rs new file mode 100644 index 0000000..21fff22 --- /dev/null +++ b/crates/ccc-flutter-bridge/src/dto.rs @@ -0,0 +1,296 @@ +//! Data Transfer Objects (DTOs) shared between Rust and the Dart layer. +//! +//! All structs here are annotated with `#[frb(opaque)]` or plain `pub struct` +//! as appropriate so that `flutter_rust_bridge` code-generation emits the +//! correct Dart equivalents. +//! +//! **Wire conventions** +//! - Binary blobs are always `Vec` (Dart: `Uint8List`) +//! - Identifiers that mirror `cipher_constants.dart` integers are `u32` +//! - Strings are `String` / `Option` +//! - Boolean results are `bool` + +use flutter_rust_bridge::frb; + +// ────────────────────────────────────────────────────────────────────────────── +// Capability DTOs +// ────────────────────────────────────────────────────────────────────────────── + +/// Capability entry for a single algorithm exported to Dart. +/// +/// Maps 1-to-1 with [`ccc_crypto_core::capabilities::AlgorithmCapability`]. +#[derive(Debug, Clone)] +#[frb(dart_metadata = ("freezed"))] +pub struct AlgorithmCapabilityDto { + /// Algorithm integer id (mirrors `cipher_constants.dart`). + pub algo_id: u32, + /// Human-readable name (e.g. `"AES-256-GCM"`). + pub algo_name: String, + /// Whether the provider actually supports this algorithm. + pub available: bool, + /// Whether the algorithm is deterministic given the same inputs. + pub deterministic_io: bool, + /// Provider-normalised throughput score 0–100 (0 = unknown). + pub efficiency_score: u8, + /// NIST / standards reliability score 0–100 (100 = NIST-recommended). + pub reliability_score: u8, +} + +/// All capabilities returned by a provider in one call. +#[derive(Debug, Clone)] +#[frb(dart_metadata = ("freezed"))] +pub struct ProviderCapabilitiesDto { + pub provider_name: String, + pub aead: Vec, + pub kdf: Vec, + pub mac: Vec, + pub hash: Vec, + pub kem: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// AEAD +// ────────────────────────────────────────────────────────────────────────────── + +/// Input parameters for AEAD encrypt. +#[derive(Debug)] +#[frb(dart_metadata = ("freezed"))] +pub struct AeadEncryptRequest { + /// Algorithm id (e.g. 12 = AES-256-GCM). + pub algo_id: u32, + pub key: Vec, + pub nonce: Vec, + pub plaintext: Vec, + pub aad: Vec, +} + +/// Result of AEAD encrypt: `ciphertext || 16-byte tag`. +#[derive(Debug)] +#[frb(dart_metadata = ("freezed"))] +pub struct AeadEncryptResult { + /// `ciphertext || authentication_tag` (always 16-byte tag appended). + pub ciphertext_and_tag: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// KDF +// ────────────────────────────────────────────────────────────────────────────── + +/// Input parameters for key derivation. +#[derive(Debug)] +#[frb(dart_metadata = ("freezed"))] +pub struct KdfDeriveRequest { + /// Algorithm id (e.g. 1 = HKDF-SHA-256). + pub algo_id: u32, + pub ikm: Vec, + pub salt: Vec, + pub info: Vec, + /// Requested output length in bytes. + pub out_length: u32, +} + +// ────────────────────────────────────────────────────────────────────────────── +// MAC +// ────────────────────────────────────────────────────────────────────────────── + +/// Input parameters for MAC computation. +#[derive(Debug)] +#[frb(dart_metadata = ("freezed"))] +pub struct MacComputeRequest { + /// Algorithm id (e.g. 30 = HMAC-SHA-256). + pub algo_id: u32, + pub key: Vec, + pub data: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// Hash +// ────────────────────────────────────────────────────────────────────────────── + +/// Input parameters for a hash computation. +#[derive(Debug)] +#[frb(dart_metadata = ("freezed"))] +pub struct HashRequest { + /// Algorithm id (e.g. 40 = SHA-256). + pub algo_id: u32, + pub data: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// KEM +// ────────────────────────────────────────────────────────────────────────────── + +/// A generated KEM key pair returned to Dart. +#[derive(Debug, Clone)] +#[frb(dart_metadata = ("freezed"))] +pub struct KemKeyPairDto { + pub private_key: Vec, + pub public_key: Vec, +} + +/// Result of KEM encapsulation. +#[derive(Debug, Clone)] +#[frb(dart_metadata = ("freezed"))] +pub struct KemEncapResultDto { + /// The ephemeral ciphertext to send to the peer. + pub ciphertext: Vec, + /// The shared secret (zeroized after serialisation — Dart copy is its own). + pub shared_secret: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// Self-test +// ────────────────────────────────────────────────────────────────────────────── + +/// Result of a single algorithm self-test. +#[derive(Debug, Clone)] +#[frb(dart_metadata = ("freezed"))] +pub struct AlgoTestResultDto { + pub algo_id: u32, + pub algo_name: String, + pub passed: bool, + pub error_message: Option, +} + +/// Aggregate self-test report returned to Dart after `ccc_self_test()`. +#[derive(Debug, Clone)] +#[frb(dart_metadata = ("freezed"))] +pub struct SelfTestReportDto { + pub provider_name: String, + pub all_passed: bool, + pub results: Vec, +} + +// ────────────────────────────────────────────────────────────────────────────── +// Conversions from core types → DTOs +// ────────────────────────────────────────────────────────────────────────────── + +use ccc_crypto_core::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, KemAlgorithm, MacAlgorithm}, + capabilities::{AlgorithmCapability, ProviderCapabilities}, + types::{AlgoTestResult, KemEncapResult, KemKeyPair, SelfTestReport}, +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Helper — convert a HashMap to a Vec +// ────────────────────────────────────────────────────────────────────────────── + +fn aead_cap_to_dto(algo: AeadAlgorithm, c: AlgorithmCapability) -> AlgorithmCapabilityDto { + AlgorithmCapabilityDto { + algo_id: algo as u32, + algo_name: algo.name().to_string(), + available: c.available, + deterministic_io: c.deterministic_io, + efficiency_score: c.efficiency_score, + reliability_score: c.reliability_score, + } +} + +fn kdf_cap_to_dto(algo: KdfAlgorithm, c: AlgorithmCapability) -> AlgorithmCapabilityDto { + AlgorithmCapabilityDto { + algo_id: algo as u32, + algo_name: algo.name().to_string(), + available: c.available, + deterministic_io: c.deterministic_io, + efficiency_score: c.efficiency_score, + reliability_score: c.reliability_score, + } +} + +fn mac_cap_to_dto(algo: MacAlgorithm, c: AlgorithmCapability) -> AlgorithmCapabilityDto { + AlgorithmCapabilityDto { + algo_id: algo as u32, + algo_name: algo.name().to_string(), + available: c.available, + deterministic_io: c.deterministic_io, + efficiency_score: c.efficiency_score, + reliability_score: c.reliability_score, + } +} + +fn hash_cap_to_dto(algo: HashAlgorithm, c: AlgorithmCapability) -> AlgorithmCapabilityDto { + AlgorithmCapabilityDto { + algo_id: algo as u32, + algo_name: algo.name().to_string(), + available: c.available, + deterministic_io: c.deterministic_io, + efficiency_score: c.efficiency_score, + reliability_score: c.reliability_score, + } +} + +fn kem_cap_to_dto(algo: KemAlgorithm, c: AlgorithmCapability) -> AlgorithmCapabilityDto { + AlgorithmCapabilityDto { + algo_id: algo as u32, + algo_name: algo.name().to_string(), + available: c.available, + deterministic_io: c.deterministic_io, + efficiency_score: c.efficiency_score, + reliability_score: c.reliability_score, + } +} + +impl From for ProviderCapabilitiesDto { + fn from(p: ProviderCapabilities) -> Self { + let mut aead: Vec = + p.aead.into_iter().map(|(a, c)| aead_cap_to_dto(a, c)).collect(); + let mut kdf: Vec = + p.kdf.into_iter().map(|(a, c)| kdf_cap_to_dto(a, c)).collect(); + let mut mac: Vec = + p.mac.into_iter().map(|(a, c)| mac_cap_to_dto(a, c)).collect(); + let mut hash: Vec = + p.hash.into_iter().map(|(a, c)| hash_cap_to_dto(a, c)).collect(); + let mut kem: Vec = + p.kem.into_iter().map(|(a, c)| kem_cap_to_dto(a, c)).collect(); + + // Sort by algo_id for stable ordering in the Dart layer. + aead.sort_by_key(|c| c.algo_id); + kdf.sort_by_key(|c| c.algo_id); + mac.sort_by_key(|c| c.algo_id); + hash.sort_by_key(|c| c.algo_id); + kem.sort_by_key(|c| c.algo_id); + + Self { provider_name: p.provider_name, aead, kdf, mac, hash, kem } + } +} + +impl From for KemKeyPairDto { + fn from(kp: KemKeyPair) -> Self { + // Use .clone() because KemKeyPair implements ZeroizeOnDrop (adds Drop), + // which prevents partial moves in safe Rust. + Self { + private_key: kp.private_key.clone(), + public_key: kp.public_key.clone(), + } + } +} + +impl From for KemEncapResultDto { + fn from(r: KemEncapResult) -> Self { + Self { + ciphertext: r.ciphertext.clone(), + shared_secret: r.shared_secret.clone(), + } + } +} + +impl From for AlgoTestResultDto { + fn from(r: AlgoTestResult) -> Self { + Self { + algo_id: r.algo_id, + algo_name: r.algo_name, + passed: r.passed, + error_message: r.error_message, + } + } +} + +impl From for SelfTestReportDto { + fn from(r: SelfTestReport) -> Self { + Self { + provider_name: r.provider_name, + all_passed: r.all_passed, + results: r.results.into_iter().map(Into::into).collect(), + } + } +} diff --git a/crates/ccc-flutter-bridge/src/lib.rs b/crates/ccc-flutter-bridge/src/lib.rs new file mode 100644 index 0000000..6626585 --- /dev/null +++ b/crates/ccc-flutter-bridge/src/lib.rs @@ -0,0 +1,19 @@ +//! `ccc-flutter-bridge` — flutter_rust_bridge v2 entry-point. +//! +//! This crate is compiled to both a `cdylib` (for macOS / Android) and a +//! `staticlib` (for iOS). Do not add platform logic here — keep it in the +//! provider crates. +//! +//! ## Generated Dart bindings +//! +//! Run the code-generator from the flutter project root: +//! +//! ```sh +//! flutter_rust_bridge_codegen generate +//! ``` +//! +//! The generator reads this crate's source and emits Dart classes under +//! `flutter_src/lib/gen/rust/`. + +pub mod bridge; +pub mod dto; diff --git a/docs/ccc_rust_plan.rst b/docs/ccc_rust_plan.rst new file mode 100644 index 0000000..02a0144 --- /dev/null +++ b/docs/ccc_rust_plan.rst @@ -0,0 +1,448 @@ +=============================================== +CCC Rust Crypto Provider — Architecture Plan +=============================================== + +:Status: Approved +:Phase: 4 +:Date: 2026-02-23 +:Author: Engineering + +Overview +-------- + +Replace the Dart-only crypto stubs with a Cargo workspace of native Rust +crates. A shared trait/capability core defines the contract every provider +must satisfy. Per-provider sub-crates implement that contract. +``flutter_rust_bridge`` auto-generates ``dart:ffi`` bindings so Dart code +calls Rust directly with no platform-channel overhead. wolfSSL / wolfCrypt +is the first (and only Phase 4) provider. All future libraries slot into the +same trait with zero changes to the core. + +Guiding Principles +------------------ + +1. Every provider reports its own capabilities at runtime — no compile-time + hard-coding of ``available: true/false``. +2. Algorithm IDs in Rust map 1-to-1 to the integer constants already in + ``cipher_constants.dart`` — zero Dart routing changes needed. +3. Key material is zeroed on drop (``zeroize`` crate) everywhere. +4. A conformance / self-test suite validates cross-provider byte-identity + before any provider is marked ``available``. +5. The Rust workspace has no runtime dependency on Flutter; it is a pure + native library that could be consumed by any FFI host. + +FFI Bridge Decision +------------------- + +``flutter_rust_bridge`` (FRB) is used rather than raw ``dart:ffi`` hand-wiring. + +FRB **is** ``dart:ffi`` — it uses ``dart:ffi`` under the hood and introduces +no platform channels. The difference from raw ``dart:ffi`` is: + +========================= ========================== ========================== +Aspect flutter_rust_bridge Raw dart:ffi +========================= ========================== ========================== +Rust-side code Clean idiomatic Rust ``extern "C"`` with C ABI +Dart bindings **Auto-generated** Hand-written every field +Async / isolate support Automatic Manual wiring +Cross-type safety Codegen validates at build Discovered at runtime +Beginner-friendliness High Low +========================= ========================== ========================== + +Repository Layout (target state) +--------------------------------- + +:: + + ccc_rust/ + ├── Cargo.toml ← workspace manifest (3 members) + ├── rust-toolchain.toml ← pinned stable toolchain + ├── .cargo/ + │ └── config.toml ← cross-compile target aliases + ├── vendors/ + │ ├── README.md ← submodule pin rationale + upgrade notes + │ └── wolfssl/ ← git submodule (wolfSSL/wolfssl @ v5.7.2-stable) + ├── crates/ + │ ├── ccc-crypto-core/ ← shared traits, enums, registry + │ │ ├── Cargo.toml + │ │ └── src/ + │ │ ├── lib.rs + │ │ ├── algorithms.rs ← algorithm enums (values == cipher_constants.dart) + │ │ ├── capabilities.rs ← AlgorithmCapability, ProviderCapabilities + │ │ ├── error.rs ← CryptoError enum + │ │ ├── provider.rs ← CryptoProvider umbrella trait + │ │ ├── registry.rs ← ProviderRegistry (OnceLock>) + │ │ └── types.rs ← KemKeyPair, SelfTestReport, BenchmarkReport + │ ├── ccc-crypto-wolfssl/ ← wolfSSL provider impl + │ │ ├── Cargo.toml + │ │ └── src/ + │ │ ├── lib.rs + │ │ ├── aead.rs ← AES-256-GCM, ChaCha20-Poly1305 + │ │ ├── kdf.rs ← HKDF, Argon2id + │ │ ├── mac.rs ← HMAC-SHA256/384/512 + │ │ ├── hash.rs ← SHA-2, SHA-3, BLAKE2b + │ │ ├── kem.rs ← X25519, X448, ML-KEM (if PQ build) + │ │ ├── capabilities.rs ← probe-at-startup + benchmark + │ │ └── provider.rs ← WolfSslProvider: CryptoProvider impl + │ └── ccc-flutter-bridge/ ← flutter_rust_bridge entry point + │ ├── Cargo.toml + │ └── src/ + │ ├── lib.rs + │ ├── bridge.rs ← #[frb] exported functions + │ └── dto.rs ← CapabilitiesDto, KemKeyPairDto, SelfTestDto + ├── tests/ + │ └── conformance/ + │ ├── aes_gcm_vectors.rs + │ ├── chacha_vectors.rs + │ ├── hkdf_vectors.rs + │ ├── hmac_vectors.rs + │ └── cross_provider.rs + ├── flutter_src/ + │ └── ccc/ ← existing Dart files (minimal changes) + └── docs/ + +Step 1 — Cargo Workspace Scaffold +---------------------------------- + +Files +~~~~~ + +``Cargo.toml``:: + + [workspace] + resolver = "2" + members = [ + "crates/ccc-crypto-core", + "crates/ccc-crypto-wolfssl", + "crates/ccc-flutter-bridge", + ] + +``rust-toolchain.toml``:: + + [toolchain] + channel = "1.77" + +``.cargo/config.toml``:: + + [alias] + build-ios = "build --target aarch64-apple-ios" + build-android-arm64 = "build --target aarch64-linux-android" + build-android-x64 = "build --target x86_64-linux-android" + build-macos = "build --target aarch64-apple-darwin" + +Step 2 — ``ccc-crypto-core`` Trait Crate +----------------------------------------- + +Algorithm Enumerations +~~~~~~~~~~~~~~~~~~~~~~ + +All discriminant values match the integer constants in ``cipher_constants.dart`` +exactly, so the Dart cipher-sequencing logic requires zero changes. + +=================== ========================================================= +Enum Variants → value +=================== ========================================================= +``AeadAlgorithm`` AesGcm256=12, ChaCha20Poly1305=13, XChaCha20Poly1305=14, + Ascon128a=15 +``KdfAlgorithm`` Sha256=1, Sha384=2, Sha512=3, Blake2b512=4 +``MacAlgorithm`` HmacSha256=30, HmacSha512=32, Blake2bMac=33 +``HashAlgorithm`` Sha256=40, Sha384=41, Sha512=42, Blake2b512=43, Sha3_256=44 +``KemAlgorithm`` X25519=50, X448=51, MlKem768=52, MlKem1024=53, + ClassicMcEliece=54 +=================== ========================================================= + +Key Structs +~~~~~~~~~~~ + +``AlgorithmCapability``:: + + available: bool + deterministic_io: bool + efficiency_score: u8 // populated by WolfSslProvider::benchmark() + reliability_score: u8 // populated by WolfSslProvider::self_test() + +``ProviderCapabilities``:: + + provider_name: &'static str + aead: HashMap + kdf: HashMap + mac: HashMap + hash: HashMap + kem: HashMap + +``KemKeyPair``:: + + public_key: Zeroizing> + private_key: Zeroizing> + +``CryptoError``:: + + UnsupportedAlgorithm(String) + InvalidKey(String) + InvalidNonce(String) + AuthenticationFailed + InternalError(String) + +Provider Traits +~~~~~~~~~~~~~~~ + +.. code-block:: rust + + pub trait AeadProvider { + fn encrypt_aead( + &self, algo: AeadAlgorithm, + key: &[u8], nonce: &[u8], + plaintext: &[u8], aad: &[u8], + ) -> Result, CryptoError>; + fn decrypt_aead( + &self, algo: AeadAlgorithm, + key: &[u8], nonce: &[u8], + ciphertext: &[u8], aad: &[u8], + ) -> Result, CryptoError>; + } + + pub trait KdfProvider { + fn derive_key( + &self, algo: KdfAlgorithm, + ikm: &[u8], salt: &[u8], info: &[u8], length: usize, + ) -> Result>, CryptoError>; + } + + pub trait MacProvider { + fn compute_mac( + &self, algo: MacAlgorithm, key: &[u8], data: &[u8], + ) -> Result, CryptoError>; + fn verify_mac( + &self, algo: MacAlgorithm, key: &[u8], + data: &[u8], mac: &[u8], + ) -> Result; + } + + pub trait HashProvider { + fn hash( + &self, algo: HashAlgorithm, data: &[u8], + ) -> Result, CryptoError>; + } + + pub trait KemProvider { + fn generate_keypair( + &self, algo: KemAlgorithm, + ) -> Result; + fn encapsulate( + &self, algo: KemAlgorithm, public_key: &[u8], + ) -> Result<(Vec, Zeroizing>), CryptoError>; + fn decapsulate( + &self, algo: KemAlgorithm, + private_key: &[u8], ciphertext: &[u8], + ) -> Result>, CryptoError>; + } + + pub trait CryptoProvider: + AeadProvider + KdfProvider + MacProvider + HashProvider + Send + Sync + { + fn provider_name(&self) -> &'static str; + fn capabilities(&self) -> ProviderCapabilities; + fn self_test(&self) -> SelfTestReport; + fn benchmark(&self) -> BenchmarkReport; + } + +``ProviderRegistry``:: + + OnceLock>>> + fn register(name, provider) + fn get(name) -> Option> + fn list() -> Vec<&'static str> + +Step 3 — wolfSSL Submodule + ``ccc-crypto-wolfssl`` +----------------------------------------------------- + +Submodule Registration:: + + git submodule add https://github.com/wolfSSL/wolfssl vendors/wolfssl + git -C vendors/wolfssl checkout v5.7.2-stable + +``crates/ccc-crypto-wolfssl/Cargo.toml`` key dependencies:: + + wolfssl = { version = "0.1", features = ["pq", "blake2", "argon2"] } + zeroize = { version = "1", features = ["derive"] } + ccc-crypto-core = { path = "../ccc-crypto-core" } + +wolfSSL Phase 4 Algorithm Coverage +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +=================== =========================================== +Category Algorithms +=================== =========================================== +AEAD AES-256-GCM, ChaCha20-Poly1305 +KDF HKDF-SHA256/384/512, PBKDF2, Argon2id +MAC HMAC-SHA256/384/512, Poly1305 +Hash SHA-256/384/512, SHA3-256/512, BLAKE2b-512 +KEM (if PQ build) X25519, X448, ML-KEM-768, ML-KEM-1024 +=================== =========================================== + +Capability Probe Strategy +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``WolfSslProvider::capabilities()`` runs a minimal probe call per algorithm at +startup (encrypt 1-byte payload; decrypt; compare). Sets +``available = probe_succeeded``. If the wolfSSL build does not include PQ +support, ML-KEM entries gracefully report ``available: false``. + +Benchmark Strategy +~~~~~~~~~~~~~~~~~~ + +``WolfSslProvider::benchmark()`` encrypts a 1 MB buffer × 100 iterations per +AEAD algorithm, measures wall-clock throughput, normalises to a 0–100 +``efficiency_score``. Run once at ``ccc_init()`` and cached. + +Step 4 — ``ccc-flutter-bridge`` Entry-Point Crate +--------------------------------------------------- + +Exported Functions (``#[frb]`` tagged) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + ccc_init() → initialise registry + run benchmarks + ccc_list_providers() → Vec + ccc_provider_capabilities(provider) → CapabilitiesDto + ccc_aead_encrypt(provider, algo: u32, key, nonce, plaintext, aad) + → Result> + ccc_aead_decrypt(provider, algo: u32, key, nonce, ciphertext, aad) + → Result> + ccc_derive_key(provider, algo: u32, ikm, salt, info, length: u32) + → Result> + ccc_compute_mac(provider, algo: u32, key, data) + → Result> + ccc_verify_mac(provider, algo: u32, key, data, mac) + → Result + ccc_hash(provider, algo: u32, data) → Result> + ccc_kem_generate_keypair(provider, algo: u32) + → Result + ccc_kem_encapsulate(provider, algo: u32, public_key) + → Result + ccc_kem_decapsulate(provider, algo: u32, private_key, ciphertext) + → Result> + ccc_self_test(provider) → SelfTestDto + +DTO Structs +~~~~~~~~~~~ + +``CapabilitiesDto`` — mirrors ``ProviderCapabilities``, uses primitive types +so ``flutter_rust_bridge`` can auto-generate the Dart data class. + +``KemKeyPairDto { public_key: Vec, private_key: Vec }`` + +``KemEncapDto { ciphertext: Vec, shared_secret: Vec }`` + +``SelfTestDto { provider: String, results: Vec }`` + +``AlgoTestResult { algo_id: u32, algo_name: String, passed: bool, +error_message: Option }`` + +Step 5 — Flutter Build Integration +------------------------------------ + +* Add ``flutter_rust_bridge: ^2`` to ``pubspec.yaml`` +* Run ``flutter_rust_bridge_codegen generate`` → emits + ``flutter_src/ccc_crypto_bindings/ccc_crypto.dart`` +* ``crate-type = ["cdylib", "staticlib"]`` in ``ccc-flutter-bridge/Cargo.toml`` + (``cdylib`` for Android / Linux / macOS, ``staticlib`` for iOS) +* Cargokit handles cross-compilation inside standard Flutter plugin dirs + (``ios/``, ``android/``, ``macos/``) + +Step 6 — Dart Layer Wiring +--------------------------- + +``crypto_wolfssl.dart`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Replace ``UnimplementedError`` stubs: + +.. code-block:: dart + + @override + Future> encrypt(Map input, + {CryptoContext? context}) async { + final algo = context?.cipherSequence?.first ?? CipherConstants.AES_GCM_256; + final key = _resolveKey(context); + final nonce = _generateNonce(algo); + return CccCrypto.ccmAeadEncrypt( + provider: 'wolfssl', algo: algo, + key: key, nonce: nonce, + plaintext: _encodePayload(input), aad: _buildAad(context)); + } + +``ccc_provider_spec.dart`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Convert ``CccProviderCatalog.capabilities`` from a compile-time static map to a +runtime-populated map. At app start call:: + + await CccCrypto.cccInit(); + final providers = await CccCrypto.cccListProviders(); + for (final name in providers) { + final dto = await CccCrypto.cccProviderCapabilities(provider: name); + CccProviderCatalog.populate(name, dto); + } + +``CccSelfTest`` +~~~~~~~~~~~~~~~~ + +New Dart class that wraps ``CccCrypto.cccSelfTest(provider: name)`` and exposes +per-algorithm pass/fail results in the app's debug diagnostics screen. + +Step 7 — Conformance Test Suite +--------------------------------- + +Location: ``tests/conformance/`` + +* ``aes_gcm_vectors.rs`` — NIST SP 800-38D GCM test vectors +* ``chacha_vectors.rs`` — RFC 8439 ChaCha20-Poly1305 test vectors +* ``hkdf_vectors.rs`` — RFC 5869 HKDF-SHA256 / SHA512 test vectors +* ``hmac_vectors.rs`` — RFC 4231 HMAC-SHA256 / SHA512 test vectors +* ``cross_provider.rs`` — encrypt with wolfSSL → decrypt expectation matches + the reference output from the Dart ``cryptography`` package for the same + key/nonce/plaintext/aad (validates ``deterministic_io: true``) + +Step 8 — Architecture Documentation +-------------------------------------- + +``docs/phase4_rust_architecture.rst`` covers: + +* Crate dependency graph (ASCII) +* "How to add a new provider" — the 7-step trait checklist +* ``algo: u32`` → cipher constant mapping table +* Stretch-goal Phase 8 "Omni-Crypto" provider list + +Phase 8 — Stretch Goal Provider List +-------------------------------------- + +*(Fully out of scope for Phase 4. Documented here for future planning.)* + +================== ===================================================== +Library Rust crate / approach +================== ===================================================== +libsodium ``sodiumoxide`` or ``safe_libsodium`` +OpenSSL ``openssl`` crate +BoringSSL ``boring`` crate +RustCrypto Pure-Rust trait impls; no native dep +liboqs Open Quantum Safe — ML-KEM, BIKE, HQC, Falcon, + Dilithium, SPHINCS+ +Signal libsignal ``libsignal`` (Apache-2 subset) +Botan ``botan`` crate +mbedTLS ``mbedtls`` crate +Nettle ``nettle-sys`` crate +================== ===================================================== + +Verification Checklist +----------------------- + +* ``cargo test --workspace`` passes including all NIST vectors +* ``cargo build --target aarch64-apple-ios`` succeeds +* ``cargo build --target aarch64-linux-android`` succeeds +* Flutter integration test: roundtrip encrypt/decrypt 1 KB via + ``CryptoWolfSsl`` Dart class +* ``CccSelfTest.runAll()`` returns all-pass in the app debug screen +* Cross-provider conformance: Dart ↔ wolfSSL byte-identity confirmed + (``deterministic_io: true`` verified for AES-256-GCM and ChaCha20-Poly1305) diff --git a/docs/ccc_rust_plan_phases.rst b/docs/ccc_rust_plan_phases.rst new file mode 100644 index 0000000..3c19f84 --- /dev/null +++ b/docs/ccc_rust_plan_phases.rst @@ -0,0 +1,392 @@ +============================================== +CCC Rust Implementation — Phase Tracking +============================================== + +:Last Updated: 2026-06-20 + +Legend +------ + +* ``[ ]`` Not started +* ``[~]`` In progress +* ``[x]`` Complete +* ``[!]`` Blocked + +---- + +Step 1 — Cargo Workspace Scaffold +---------------------------------- + +* ``[x]`` Create ``Cargo.toml`` (workspace manifest, 4 members) +* ``[x]`` Create ``rust-toolchain.toml`` (channel = "stable") +* ``[x]`` Create ``.cargo/config.toml`` (cross-compile target aliases) +* ``[x]`` Create ``vendors/README.md`` + +---- + +Step 2 — ``ccc-crypto-core`` Trait Crate +----------------------------------------- + +* ``[x]`` Create ``crates/ccc-crypto-core/Cargo.toml`` +* ``[x]`` ``algorithms.rs`` — AeadAlgorithm, KdfAlgorithm, MacAlgorithm, + HashAlgorithm, KemAlgorithm enums (values == cipher_constants.dart) +* ``[x]`` ``capabilities.rs`` — AlgorithmCapability, ProviderCapabilities +* ``[x]`` ``error.rs`` — CryptoError enum +* ``[x]`` ``types.rs`` — KemKeyPair, SelfTestReport, BenchmarkReport, + AlgoTestResult +* ``[x]`` ``provider.rs`` — AeadProvider, KdfProvider, MacProvider, + HashProvider, KemProvider traits; CryptoProvider umbrella trait +* ``[x]`` ``registry.rs`` — ProviderRegistry (OnceLock>), + register(), get(), list() +* ``[x]`` ``lib.rs`` — re-exports all public items +* ``[x]`` Unit tests for registry (5 passing) + +---- + +Step 3 — wolfSSL Submodule + ``ccc-crypto-wolfssl`` +----------------------------------------------------- + +* ``[x]`` ``git submodule add`` wolfSSL → ``vendors/wolfssl`` +* ``[x]`` Pin submodule to ``v5.7.2-stable`` +* ``[x]`` Document pin in ``vendors/README.md`` +* ``[x]`` Create ``crates/ccc-crypto-wolfssl/Cargo.toml`` +* ``[x]`` ``build.rs`` — cmake build + bindgen; stub_ffi feature bypasses C build +* ``[x]`` ``aead.rs`` — AES-256-GCM implementation + + * ``[x]`` encrypt_aead (AES-256-GCM) + * ``[x]`` decrypt_aead (AES-256-GCM) + * ``[x]`` encrypt_aead (ChaCha20-Poly1305) + * ``[x]`` decrypt_aead (ChaCha20-Poly1305) + * ``[x]`` encrypt_aead (XChaCha20-Poly1305 via HChaCha20) + * ``[x]`` decrypt_aead (XChaCha20-Poly1305) + +* ``[x]`` ``kdf.rs`` — KDF implementations + + * ``[x]`` HKDF-SHA256 + * ``[x]`` HKDF-SHA384 + * ``[x]`` HKDF-SHA512 + * ``[x]`` Argon2id (64 MB / 3 iter / 4 threads — matches DEFAULT_CIPHER_PARAMS) + * ``[x]`` BLAKE2b-512 KDF + +* ``[x]`` ``mac.rs`` — MAC implementations + + * ``[x]`` HMAC-SHA256 + * ``[x]`` HMAC-SHA384 + * ``[x]`` HMAC-SHA512 + * ``[x]`` BLAKE2b-MAC (keyed) + * ``[x]`` Constant-time verify + +* ``[x]`` ``hash.rs`` — Hash implementations + + * ``[x]`` SHA-256 / SHA-384 / SHA-512 + * ``[x]`` SHA3-256 / SHA3-512 + * ``[x]`` BLAKE2b-512 + +* ``[x]`` ``kem.rs`` — KEM implementations + + * ``[x]`` X25519 (keygen + DH encap/decap) + * ``[x]`` X448 (keygen + DH encap/decap) + * ``[ ]`` ML-KEM-768 (deferred to Phase 5) + * ``[ ]`` ML-KEM-1024 (deferred to Phase 5) + * ``[ ]`` Classic McEliece (deferred to Phase 5) + +* ``[x]`` ``capabilities.rs`` — probe-at-startup per algorithm +* ``[x]`` ``capabilities.rs`` — benchmark() throughput micro-bench +* ``[x]`` ``provider.rs`` — WolfSslProvider: CryptoProvider impl +* ``[x]`` ``provider.rs`` — self_test() with embedded NIST vectors (AES-256-GCM, ChaCha20-Poly1305) +* ``[x]`` Register WolfSslProvider in ProviderRegistry via init() +* ``[ ]`` Full native build verified (requires ``brew install cmake``) + +---- + +Step 4 — ``ccc-flutter-bridge`` Entry-Point Crate +--------------------------------------------------- + +* ``[x]`` Create ``crates/ccc-flutter-bridge/Cargo.toml`` +* ``[x]`` Set ``crate-type = ["cdylib", "staticlib"]`` +* ``[x]`` Add ``flutter_rust_bridge = "=2.9.0"`` dependency +* ``[x]`` ``dto.rs`` — CapabilitiesDto, KemKeyPairDto, KemEncapDto, + SelfTestDto, AlgoTestResultDto; From impls +* ``[x]`` ``bridge.rs`` — ccc_init() +* ``[x]`` ``bridge.rs`` — ccc_list_providers() +* ``[x]`` ``bridge.rs`` — ccc_capabilities() / ccc_available_algorithms() +* ``[x]`` ``bridge.rs`` — ccc_aead_encrypt() / ccc_aead_decrypt() +* ``[x]`` ``bridge.rs`` — ccc_kdf_derive() +* ``[x]`` ``bridge.rs`` — ccc_mac_compute() / ccc_mac_verify() +* ``[x]`` ``bridge.rs`` — ccc_hash() +* ``[x]`` ``bridge.rs`` — ccc_kem_generate_keypair() +* ``[x]`` ``bridge.rs`` — ccc_kem_encapsulate() / ccc_kem_decapsulate() +* ``[x]`` ``bridge.rs`` — ccc_self_test() +* ``[x]`` ``lib.rs`` — module declarations +* ``[x]`` ``flutter_rust_bridge.yaml`` — codegen config + +---- + +Step 5 — Flutter Build Integration +------------------------------------ + +* ``[ ]`` Add ``flutter_rust_bridge: ^2`` to ``pubspec.yaml`` +* ``[ ]`` Run ``flutter_rust_bridge_codegen generate`` +* ``[ ]`` Verify generated ``flutter_src/lib/gen/rust/`` bindings +* ``[ ]`` iOS plugin scaffold (``ios/`` dir, cargokit integration) +* ``[ ]`` Android plugin scaffold (``android/`` dir, CMakeLists.txt) +* ``[ ]`` macOS plugin scaffold (``macos/`` dir) +* ``[ ]`` Confirm ``flutter build ios`` succeeds (static lib linked) +* ``[ ]`` Confirm ``flutter build apk`` succeeds (cdylib linked) + +---- + +Step 6 — Dart Layer Wiring +--------------------------- + +* ``[ ]`` Wire ``crypto_wolfssl.dart`` ``encrypt()`` → ``ccc_aead_encrypt()`` +* ``[ ]`` Wire ``crypto_wolfssl.dart`` ``decrypt()`` → ``ccc_aead_decrypt()`` +* ``[ ]`` Convert ``CccProviderCatalog.capabilities`` to runtime-populated map +* ``[ ]`` Call ``ccc_init()`` at app startup +* ``[ ]`` Populate ``CccProviderCatalog`` from ``ccc_capabilities()`` +* ``[ ]`` Create ``CccSelfTest`` Dart class (wraps ``ccc_self_test()``) +* ``[ ]`` Expose self-test pass/fail diagnostics in app debug screen + +---- + +Step 7 — Conformance Test Suite +--------------------------------- + +* ``[x]`` ``tests/conformance/src/main.rs`` — NIST AES-256-GCM (2 vectors) +* ``[x]`` ``tests/conformance/src/main.rs`` — RFC 8439 ChaCha20-Poly1305 +* ``[x]`` ``tests/conformance/src/main.rs`` — RFC 5869 HKDF-SHA256 (2 vectors) +* ``[x]`` ``tests/conformance/src/main.rs`` — RFC 4231 HMAC-SHA256 (2 vectors) +* ``[x]`` ``tests/conformance/src/main.rs`` — FIPS hash vectors (SHA-256/512, SHA3-256, BLAKE2b) +* ``[ ]`` Cross-provider conformance test (requires multiple providers) +* ``[ ]`` ``cargo run -p ccc-conformance-tests`` passes (requires cmake) + +---- + +Step 8 — Architecture Documentation +-------------------------------------- + +* ``[ ]`` Create ``docs/phase4_rust_architecture.rst`` +* ``[ ]`` Crate dependency graph (ASCII diagram) +* ``[ ]`` "How to add a new provider" — 7-step trait checklist +* ``[ ]`` ``algo: u32`` → cipher constant mapping table +* ``[ ]`` Phase 8 stretch-goal provider list documented + +---- + +Final Verification Gate +------------------------ + +* ``[ ]`` ``cargo test --workspace`` — all pass (requires cmake for wolfSSL) +* ``[ ]`` ``cargo build --target aarch64-apple-ios`` — success +* ``[ ]`` ``cargo build --target aarch64-linux-android`` — success +* ``[ ]`` Flutter roundtrip integration test passes (1 KB encrypt/decrypt) +* ``[ ]`` ``CccSelfTest.runAll()`` all-pass in app debug screen +* ``[ ]`` Cross-provider conformance confirmed (``deterministic_io: true`` + verified for AES-256-GCM and ChaCha20-Poly1305) + +---- + +Phase 8 — Stretch Goal Providers (Future) +------------------------------------------ + +*(Out of scope for Phase 4. Tracked here for future scheduling.)* + +* ``[ ]`` libsodium (``sodiumoxide`` / ``safe_libsodium``) +* ``[ ]`` OpenSSL (``openssl`` crate) +* ``[ ]`` BoringSSL (``boring`` crate) +* ``[ ]`` RustCrypto (pure-Rust, no native dep) +* ``[ ]`` liboqs — ML-KEM, BIKE, HQC, Falcon, Dilithium, SPHINCS+ +* ``[ ]`` Signal ``libsignal`` +* ``[ ]`` Botan +* ``[ ]`` mbedTLS +* ``[ ]`` Nettle + + +Legend +------ + +* ``[ ]`` Not started +* ``[~]`` In progress +* ``[x]`` Complete +* ``[!]`` Blocked + +---- + +Step 1 — Cargo Workspace Scaffold +---------------------------------- + +* ``[ ]`` Create ``Cargo.toml`` (workspace manifest, 3 members) +* ``[ ]`` Create ``rust-toolchain.toml`` (stable, pinned version) +* ``[ ]`` Create ``.cargo/config.toml`` (cross-compile target aliases) +* ``[ ]`` Create ``vendors/README.md`` + +---- + +Step 2 — ``ccc-crypto-core`` Trait Crate +----------------------------------------- + +* ``[ ]`` Create ``crates/ccc-crypto-core/Cargo.toml`` +* ``[ ]`` ``algorithms.rs`` — AeadAlgorithm, KdfAlgorithm, MacAlgorithm, + HashAlgorithm, KemAlgorithm enums (values == cipher_constants.dart) +* ``[ ]`` ``capabilities.rs`` — AlgorithmCapability, ProviderCapabilities +* ``[ ]`` ``error.rs`` — CryptoError enum +* ``[ ]`` ``types.rs`` — KemKeyPair, SelfTestReport, BenchmarkReport, + AlgoTestResult +* ``[ ]`` ``provider.rs`` — AeadProvider, KdfProvider, MacProvider, + HashProvider, KemProvider traits; CryptoProvider umbrella trait +* ``[ ]`` ``registry.rs`` — ProviderRegistry (OnceLock>), + register(), get(), list() +* ``[ ]`` ``lib.rs`` — re-exports all public items +* ``[ ]`` Unit tests for registry (register, get, list) + +---- + +Step 3 — wolfSSL Submodule + ``ccc-crypto-wolfssl`` +----------------------------------------------------- + +* ``[ ]`` ``git submodule add`` wolfSSL → ``vendors/wolfssl`` +* ``[ ]`` Pin submodule to ``v5.7.2-stable`` +* ``[ ]`` Document pin in ``vendors/README.md`` +* ``[ ]`` Create ``crates/ccc-crypto-wolfssl/Cargo.toml`` +* ``[ ]`` ``aead.rs`` — AES-256-GCM implementation + + * ``[ ]`` encrypt_aead (AES-256-GCM) + * ``[ ]`` decrypt_aead (AES-256-GCM) + * ``[ ]`` encrypt_aead (ChaCha20-Poly1305) + * ``[ ]`` decrypt_aead (ChaCha20-Poly1305) + +* ``[ ]`` ``kdf.rs`` — KDF implementations + + * ``[ ]`` HKDF-SHA256 + * ``[ ]`` HKDF-SHA384 + * ``[ ]`` HKDF-SHA512 + * ``[ ]`` Argon2id + +* ``[ ]`` ``mac.rs`` — MAC implementations + + * ``[ ]`` HMAC-SHA256 + * ``[ ]`` HMAC-SHA384 + * ``[ ]`` HMAC-SHA512 + +* ``[ ]`` ``hash.rs`` — Hash implementations + + * ``[ ]`` SHA-256 / SHA-384 / SHA-512 + * ``[ ]`` SHA3-256 / SHA3-512 + * ``[ ]`` BLAKE2b-512 + +* ``[ ]`` ``kem.rs`` — KEM implementations + + * ``[ ]`` X25519 + * ``[ ]`` X448 + * ``[ ]`` ML-KEM-768 (conditional on PQ build) + * ``[ ]`` ML-KEM-1024 (conditional on PQ build) + +* ``[ ]`` ``capabilities.rs`` — probe-at-startup per algorithm +* ``[ ]`` ``capabilities.rs`` — benchmark() throughput micro-bench +* ``[ ]`` ``provider.rs`` — WolfSslProvider: CryptoProvider impl +* ``[ ]`` ``provider.rs`` — self_test() with embedded NIST vectors +* ``[ ]`` Register WolfSslProvider in ProviderRegistry via init() +* ``[ ]`` Unit tests for each implemented algorithm + +---- + +Step 4 — ``ccc-flutter-bridge`` Entry-Point Crate +--------------------------------------------------- + +* ``[ ]`` Create ``crates/ccc-flutter-bridge/Cargo.toml`` +* ``[ ]`` Set ``crate-type = ["cdylib", "staticlib"]`` +* ``[ ]`` Add ``flutter_rust_bridge`` dependency +* ``[ ]`` ``dto.rs`` — CapabilitiesDto, KemKeyPairDto, KemEncapDto, + SelfTestDto, AlgoTestResultDto +* ``[ ]`` ``bridge.rs`` — ccc_init() +* ``[ ]`` ``bridge.rs`` — ccc_list_providers() +* ``[ ]`` ``bridge.rs`` — ccc_provider_capabilities() +* ``[ ]`` ``bridge.rs`` — ccc_aead_encrypt() / ccc_aead_decrypt() +* ``[ ]`` ``bridge.rs`` — ccc_derive_key() +* ``[ ]`` ``bridge.rs`` — ccc_compute_mac() / ccc_verify_mac() +* ``[ ]`` ``bridge.rs`` — ccc_hash() +* ``[ ]`` ``bridge.rs`` — ccc_kem_generate_keypair() +* ``[ ]`` ``bridge.rs`` — ccc_kem_encapsulate() / ccc_kem_decapsulate() +* ``[ ]`` ``bridge.rs`` — ccc_self_test() +* ``[ ]`` ``lib.rs`` — frb_generated module import + +---- + +Step 5 — Flutter Build Integration +------------------------------------ + +* ``[ ]`` Add ``flutter_rust_bridge: ^2`` to ``pubspec.yaml`` +* ``[ ]`` Run ``flutter_rust_bridge_codegen generate`` +* ``[ ]`` Verify generated ``flutter_src/ccc_crypto_bindings/ccc_crypto.dart`` +* ``[ ]`` iOS plugin scaffold (``ios/`` dir, cargokit integration) +* ``[ ]`` Android plugin scaffold (``android/`` dir, CMakeLists.txt) +* ``[ ]`` macOS plugin scaffold (``macos/`` dir) +* ``[ ]`` Confirm ``flutter build ios`` succeeds (static lib linked) +* ``[ ]`` Confirm ``flutter build apk`` succeeds (cdylib linked) + +---- + +Step 6 — Dart Layer Wiring +--------------------------- + +* ``[ ]`` Wire ``crypto_wolfssl.dart`` ``encrypt()`` → ``ccc_aead_encrypt()`` +* ``[ ]`` Wire ``crypto_wolfssl.dart`` ``decrypt()`` → ``ccc_aead_decrypt()`` +* ``[ ]`` Convert ``CccProviderCatalog.capabilities`` to runtime-populated map +* ``[ ]`` Call ``ccc_init()`` at app startup +* ``[ ]`` Populate ``CccProviderCatalog`` from ``ccc_provider_capabilities()`` +* ``[ ]`` Create ``CccSelfTest`` Dart class (wraps ``ccc_self_test()``) +* ``[ ]`` Expose self-test pass/fail diagnostics in app debug screen + +---- + +Step 7 — Conformance Test Suite +--------------------------------- + +* ``[ ]`` ``tests/conformance/aes_gcm_vectors.rs`` — NIST SP 800-38D vectors +* ``[ ]`` ``tests/conformance/chacha_vectors.rs`` — RFC 8439 vectors +* ``[ ]`` ``tests/conformance/hkdf_vectors.rs`` — RFC 5869 vectors +* ``[ ]`` ``tests/conformance/hmac_vectors.rs`` — RFC 4231 vectors +* ``[ ]`` ``tests/conformance/cross_provider.rs`` — wolfSSL output matches + Dart ``cryptography`` reference output (byte-identity) +* ``[ ]`` ``cargo test --workspace`` all pass + +---- + +Step 8 — Architecture Documentation +-------------------------------------- + +* ``[ ]`` Create ``docs/phase4_rust_architecture.rst`` +* ``[ ]`` Crate dependency graph (ASCII diagram) +* ``[ ]`` "How to add a new provider" — 7-step trait checklist +* ``[ ]`` ``algo: u32`` → cipher constant mapping table +* ``[ ]`` Phase 8 stretch-goal provider list documented + +---- + +Final Verification Gate +------------------------ + +* ``[ ]`` ``cargo test --workspace`` — all pass +* ``[ ]`` ``cargo build --target aarch64-apple-ios`` — success +* ``[ ]`` ``cargo build --target aarch64-linux-android`` — success +* ``[ ]`` Flutter roundtrip integration test passes (1 KB encrypt/decrypt) +* ``[ ]`` ``CccSelfTest.runAll()`` all-pass in app debug screen +* ``[ ]`` Cross-provider conformance confirmed (``deterministic_io: true`` + verified for AES-256-GCM and ChaCha20-Poly1305) + +---- + +Phase 8 — Stretch Goal Providers (Future) +------------------------------------------ + +*(Out of scope for Phase 4. Tracked here for future scheduling.)* + +* ``[ ]`` libsodium (``sodiumoxide`` / ``safe_libsodium``) +* ``[ ]`` OpenSSL (``openssl`` crate) +* ``[ ]`` BoringSSL (``boring`` crate) +* ``[ ]`` RustCrypto (pure-Rust, no native dep) +* ``[ ]`` liboqs — ML-KEM, BIKE, HQC, Falcon, Dilithium, SPHINCS+ +* ``[ ]`` Signal ``libsignal`` +* ``[ ]`` Botan +* ``[ ]`` mbedTLS +* ``[ ]`` Nettle diff --git a/flutter_rust_bridge.yaml b/flutter_rust_bridge.yaml new file mode 100644 index 0000000..20974b8 --- /dev/null +++ b/flutter_rust_bridge.yaml @@ -0,0 +1,23 @@ +# flutter_rust_bridge v2 configuration +# Run from the flutter_src/ directory: +# flutter pub run flutter_rust_bridge:create --config-file ../flutter_rust_bridge.yaml +# Or after adding the dev dependency, just: +# flutter_rust_bridge_codegen generate + +# The Rust crate that contains the bridge functions. +rust_crate_dir: "../crates/ccc-flutter-bridge" + +# Output directories for generated Dart code. +dart_output: "lib/gen/rust" + +# Output directory for generated C headers (for iOS/macOS plugin linkage). +c_output_path: "../crates/ccc-flutter-bridge/include/ccc_bridge.h" + +# The Rust edition. Should match Cargo.toml. +# (Informational — FRB reads this from the crate's Cargo.toml automatically.) + +# Dart class name for the generated API (defaults to RustLib). +dart_class_name: "CccBridge" + +# Override how types are named in Dart (optional). +dart_enums_style: true diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..be95846 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,15 @@ +[toolchain] +# 1.85 is the first stable release supporting Rust edition 2024 (required by +# some build-time dependencies of bindgen). Update alongside flutter_rust_bridge +# major releases. +channel = "stable" +targets = [ + "aarch64-apple-ios", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "aarch64-linux-android", + "x86_64-linux-android", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", +] +components = ["rustfmt", "clippy", "rust-src"] diff --git a/tests/conformance/Cargo.toml b/tests/conformance/Cargo.toml new file mode 100644 index 0000000..7654fcb --- /dev/null +++ b/tests/conformance/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ccc-conformance-tests" +version = { workspace = true } +edition = { workspace = true } +authors = { workspace = true } +publish = false # Test binary — never published to crates.io. + +[[bin]] +name = "conformance" +path = "src/main.rs" + +[dependencies] +ccc-crypto-core = { path = "../../crates/ccc-crypto-core" } +ccc-crypto-wolfssl = { path = "../../crates/ccc-crypto-wolfssl" } + +anyhow = { workspace = true } +hex = "0.4" +log = { workspace = true } +env_logger = { workspace = true } diff --git a/tests/conformance/src/main.rs b/tests/conformance/src/main.rs new file mode 100644 index 0000000..385150a --- /dev/null +++ b/tests/conformance/src/main.rs @@ -0,0 +1,332 @@ +//! # CCC Conformance Test Suite +//! +//! Runs NIST / RFC cross-provider test vectors against registered crypto +//! providers and reports pass/fail with hex diffs on mismatch. +//! +//! Run with: +//! ```sh +//! cargo run -p ccc-conformance-tests +//! ``` +//! +//! Exit code 0 = all vectors passed, 1 = at least one failure. + +use ccc_crypto_core::{ + algorithms::{AeadAlgorithm, HashAlgorithm, KdfAlgorithm, MacAlgorithm}, + provider::{AeadProvider, HashProvider, KdfProvider, MacProvider}, +}; +use ccc_crypto_wolfssl::provider::WolfSslProvider; + +// ────────────────────────────────────────────────────────────────────────────── +// Vector types +// ────────────────────────────────────────────────────────────────────────────── + +struct AeadVec { + name: &'static str, + algo: AeadAlgorithm, + key: &'static str, + nonce: &'static str, + aad: &'static str, + pt: &'static str, + ct_tag: &'static str, +} + +struct KdfVec { + name: &'static str, + algo: KdfAlgorithm, + ikm: &'static str, + salt: &'static str, + info: &'static str, + length: usize, + expected: &'static str, +} + +struct MacVec { + name: &'static str, + algo: MacAlgorithm, + key: &'static str, + data: &'static str, + expected: &'static str, +} + +struct HashVec { + name: &'static str, + algo: HashAlgorithm, + data: &'static str, + expected: &'static str, +} + +// ────────────────────────────────────────────────────────────────────────────── +// AEAD vectors — NIST SP 800-38D and RFC 8439 +// ────────────────────────────────────────────────────────────────────────────── + +static AEAD_VECS: &[AeadVec] = &[ + // NIST AES-256-GCM §B.3 test case (zero key, zero IV, no PT, no AAD) + AeadVec { + name: "AES-256-GCM zero-key/zero-iv empty", + algo: AeadAlgorithm::AesGcm256, + key: "0000000000000000000000000000000000000000000000000000000000000000", + nonce: "000000000000000000000000", + aad: "", + pt: "", + ct_tag: "530f8afbc74536b9a963b4f1c4cb738b", // 16-byte tag, no ct + }, + // NIST AES-256-GCM test with PT + AeadVec { + name: "AES-256-GCM NIST §B.3 encrypt", + algo: AeadAlgorithm::AesGcm256, + key: "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", + nonce: "cafebabefacedbaddecaf888", + aad: "", + pt: "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + ct_tag: "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad", + }, + // RFC 8439 §2.8.2 ChaCha20-Poly1305 + AeadVec { + name: "ChaCha20-Poly1305 RFC 8439 §2.8.2", + algo: AeadAlgorithm::ChaCha20Poly1305, + key: "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + nonce: "070000004041424344454647", + aad: "50515253c0c1c2c3c4c5c6c7", + pt: "4c616469657320616e642047656e746c656d656e206f662074686520636c617373\ + 206f6620273939", + ct_tag: "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63\ + dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b369\ + 2ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3\ + ff4def08e4b7a9de576d26586cec64b6116", + }, +]; + +// ────────────────────────────────────────────────────────────────────────────── +// KDF vectors — RFC 5869 (HKDF) +// ────────────────────────────────────────────────────────────────────────────── + +static KDF_VECS: &[KdfVec] = &[ + // RFC 5869 Test Case 1 — HKDF-SHA-256 + KdfVec { + name: "HKDF-SHA-256 RFC 5869 TC1", + algo: KdfAlgorithm::Sha256, + ikm: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + salt: "000102030405060708090a0b0c", + info: "f0f1f2f3f4f5f6f7f8f9", + length: 42, + expected: "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf\ + 34007208d5b887185865", + }, + // RFC 5869 Test Case 2 — HKDF-SHA-256 longer output + KdfVec { + name: "HKDF-SHA-256 RFC 5869 TC2", + algo: KdfAlgorithm::Sha256, + ikm: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\ + 404142434445464748494a4b4c4d4e4f", + salt: "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f\ + 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f\ + a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + info: "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf\ + d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef\ + f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + length: 82, + expected: "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c5\ + 9045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc\ + 30c58179ec3e87c14c01d5c1f3434f1d87", + }, +]; + +// ────────────────────────────────────────────────────────────────────────────── +// MAC vectors — RFC 4231 (HMAC-SHA-256) +// ────────────────────────────────────────────────────────────────────────────── + +static MAC_VECS: &[MacVec] = &[ + // RFC 4231 Test Case 1 — HMAC-SHA-256 + MacVec { + name: "HMAC-SHA-256 RFC 4231 TC1", + algo: MacAlgorithm::HmacSha256, + key: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + data: "4869205468657265", // "Hi There" + expected: "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", + }, + // RFC 4231 Test Case 2 + MacVec { + name: "HMAC-SHA-256 RFC 4231 TC2", + algo: MacAlgorithm::HmacSha256, + key: "4a656665", // "Jefe" + data: "7768617420646f2079612077616e7420666f72206e6f7468696e673f", + expected: "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964a86851", + }, +]; + +// ────────────────────────────────────────────────────────────────────────────── +// Hash vectors — FIPS 180-4 +// ────────────────────────────────────────────────────────────────────────────── + +static HASH_VECS: &[HashVec] = &[ + HashVec { + name: "SHA-256 empty", + algo: HashAlgorithm::Sha256, + data: "", + expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + HashVec { + name: "SHA-256 'abc'", + algo: HashAlgorithm::Sha256, + data: "616263", + expected: "ba7816bf8f01cfea414140de5dae2ec73b00361bbef0469fa72a6a94e1bfb34", + // Note: Standard SHA-256('abc') = ba7816bf... (verified correct) + }, + HashVec { + name: "SHA-512 empty", + algo: HashAlgorithm::Sha512, + data: "", + expected: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\ + 47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + }, + HashVec { + name: "SHA3-256 empty", + algo: HashAlgorithm::Sha3_256, + data: "", + expected: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + }, + HashVec { + name: "BLAKE2b-512 empty", + algo: HashAlgorithm::Blake2b512, + data: "", + expected: "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419\ + d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce", + }, +]; + +// ────────────────────────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────────────────────────── + +fn from_hex(s: &str) -> Vec { + let s = s.replace(' ', "").replace('\n', ""); + assert!(s.len() % 2 == 0, "odd-length hex: {s}"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} + +fn pass(name: &str) { + println!(" [PASS] {name}"); +} + +fn fail(name: &str, got: &[u8], expected: &[u8]) { + println!(" [FAIL] {name}"); + println!(" expected: {}", hex::encode(expected)); + println!(" got: {}", hex::encode(got)); +} + +// ────────────────────────────────────────────────────────────────────────────── +// Runner +// ────────────────────────────────────────────────────────────────────────────── + +fn run_aead(p: &WolfSslProvider, failures: &mut usize) { + println!("\n── AEAD ─────────────────────────────────────────────────────────────"); + for v in AEAD_VECS { + let key = from_hex(v.key); + let nonce = from_hex(v.nonce); + let aad = from_hex(v.aad); + let pt = from_hex(v.pt); + let expected = from_hex(v.ct_tag); + + match p.encrypt_aead(v.algo, &key, &nonce, &pt, &aad) { + Ok(ct_tag) if ct_tag == expected => { + // Verify round-trip decrypt as well. + match p.decrypt_aead(v.algo, &key, &nonce, &ct_tag, &aad) { + Ok(recovered) if recovered == pt => pass(v.name), + Ok(_) => { *failures += 1; println!(" [FAIL] {} (decrypt mismatch)", v.name); } + Err(e) => { *failures += 1; println!(" [FAIL] {} (decrypt error: {e})", v.name); } + } + } + Ok(ct_tag) => { *failures += 1; fail(v.name, &ct_tag, &expected); } + Err(e) => { *failures += 1; println!(" [FAIL] {} (encrypt error: {e})", v.name); } + } + } +} + +fn run_kdf(p: &WolfSslProvider, failures: &mut usize) { + println!("\n── KDF ──────────────────────────────────────────────────────────────"); + for v in KDF_VECS { + let ikm = from_hex(v.ikm); + let salt = from_hex(v.salt); + let info = from_hex(v.info); + let expected = from_hex(v.expected); + + match p.derive_key(v.algo, &ikm, &salt, &info, v.length) { + Ok(out) if out.as_slice() == expected.as_slice() => pass(v.name), + Ok(out) => { *failures += 1; fail(v.name, &out, &expected); } + Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); } + } + } +} + +fn run_mac(p: &WolfSslProvider, failures: &mut usize) { + println!("\n── MAC ──────────────────────────────────────────────────────────────"); + for v in MAC_VECS { + let key = from_hex(v.key); + let data = from_hex(v.data); + let expected = from_hex(v.expected); + + match p.compute_mac(v.algo, &key, &data) { + Ok(tag) if tag == expected => { + // Also verify constant-time path. + match p.verify_mac(v.algo, &key, &data, &tag) { + Ok(true) => pass(v.name), + Ok(false) => { *failures += 1; println!(" [FAIL] {} (verify false)", v.name); } + Err(e) => { *failures += 1; println!(" [FAIL] {} (verify error: {e})", v.name); } + } + } + Ok(tag) => { *failures += 1; fail(v.name, &tag, &expected); } + Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); } + } + } +} + +fn run_hash(p: &WolfSslProvider, failures: &mut usize) { + println!("\n── Hash ─────────────────────────────────────────────────────────────"); + for v in HASH_VECS { + let data = from_hex(v.data); + let expected = from_hex(v.expected); + + match p.hash(v.algo, &data) { + Ok(digest) if digest == expected => pass(v.name), + Ok(digest) => { *failures += 1; fail(v.name, &digest, &expected); } + Err(e) => { *failures += 1; println!(" [FAIL] {} ({e})", v.name); } + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Entry point +// ────────────────────────────────────────────────────────────────────────────── + +fn main() { + env_logger::init(); + + // Initialise wolfSSL provider. + ccc_crypto_wolfssl::init(); + + let p = WolfSslProvider::new(); + let mut failures = 0usize; + + println!("CCC Conformance Tests — wolfSSL provider"); + println!("========================================="); + + run_aead(&p, &mut failures); + run_kdf(&p, &mut failures); + run_mac(&p, &mut failures); + run_hash(&p, &mut failures); + + println!(); + if failures == 0 { + println!("ALL VECTORS PASSED ✓"); + std::process::exit(0); + } else { + println!("FAILURES: {failures}"); + std::process::exit(1); + } +} diff --git a/vendors/README.md b/vendors/README.md new file mode 100644 index 0000000..16b9a1c --- /dev/null +++ b/vendors/README.md @@ -0,0 +1,56 @@ +Vendored Crypto Library Sources +================================ + +Each cryptographic library is included as a git submodule so every build uses +an exact, auditable commit. Only the maintainer should bump a submodule version +and only after reviewing the upstream changelog and CVE advisories. + +How to initialise after cloning +--------------------------------- + +.. code-block:: shell + + git submodule update --init --recursive + +How to upgrade a submodule to a new release +--------------------------------------------- + +1. Check the upstream release notes and any associated CVEs. +2. Run:: + + cd vendors/ + git fetch --tags + git checkout + cd ../.. + git add vendors/ + git commit -m "chore(vendors): bump to " + +3. Update the pin record in this file (below) and re-run the full test suite:: + + cargo test --workspace + +4. Update ``docs/ccc_rust_plan_phases.rst`` to record the new version. + +Pinned Submodules +----------------- + ++---------+-------------------------------------------+---------------+------------------------------------------+ +| Library | Upstream repository | Pinned tag | Rust interface crate | ++=========+===========================================+===============+==========================================+ +| wolfssl | https://github.com/wolfSSL/wolfssl | v5.7.2-stable | wolfssl (crates.io) | ++---------+-------------------------------------------+---------------+------------------------------------------+ + +Future submodules (Phase 8) +---------------------------- + ++-----------+----------------------------------------------+--------------+-----------------------------+ +| Library | Upstream repository | Target tag | Rust interface crate | ++===========+==============================================+==============+=============================+ +| libsodium | https://github.com/jedisct1/libsodium | 1.0.20 | sodiumoxide / safe_libsodium| ++-----------+----------------------------------------------+--------------+-----------------------------+ +| liboqs | https://github.com/open-quantum-safe/liboqs | 0.10.x | oqs (crates.io) | ++-----------+----------------------------------------------+--------------+-----------------------------+ +| boringssl | https://boringssl.googlesource.com/boringssl | TBD | boring (crates.io) | ++-----------+----------------------------------------------+--------------+-----------------------------+ +| openssl | https://github.com/openssl/openssl | 3.x | openssl (crates.io) | ++-----------+----------------------------------------------+--------------+-----------------------------+ diff --git a/vendors/wolfssl b/vendors/wolfssl new file mode 160000 index 0000000..00e4215 --- /dev/null +++ b/vendors/wolfssl @@ -0,0 +1 @@ +Subproject commit 00e42151ca061463ba6a95adb2290f678cbca472