From 60ffccd19b89b7b0588be416c5f447376244cd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 16:19:49 +0100 Subject: [PATCH 01/89] Create initial Rust<->OPAQUE bindings --- extensions/Bitwarden.OPAQUE/rust/.gitignore | 1 + extensions/Bitwarden.OPAQUE/rust/Cargo.lock | 606 ++++++++++++++++++ extensions/Bitwarden.OPAQUE/rust/Cargo.toml | 17 + .../Bitwarden.OPAQUE/rust/src/client.rs | 67 ++ extensions/Bitwarden.OPAQUE/rust/src/ffi.rs | 121 ++++ .../Bitwarden.OPAQUE/rust/src/ffi_types.rs | 115 ++++ extensions/Bitwarden.OPAQUE/rust/src/lib.rs | 86 +++ .../Bitwarden.OPAQUE/rust/src/server.rs | 59 ++ .../src/Bitwarden.OPAQUE.csproj | 92 +++ .../src/BitwardenException.cs | 19 + .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 158 +++++ .../Bitwarden.OPAQUE/src/BitwardenOpaque.cs | 62 ++ extensions/Bitwarden.OPAQUE/src/bitwarden.png | Bin 0 -> 790 bytes .../tests/Bitwarden.OPAQUE.Tests.csproj | 29 + extensions/Bitwarden.OPAQUE/tests/Tests.cs | 33 + 15 files changed, 1465 insertions(+) create mode 100644 extensions/Bitwarden.OPAQUE/rust/.gitignore create mode 100644 extensions/Bitwarden.OPAQUE/rust/Cargo.lock create mode 100644 extensions/Bitwarden.OPAQUE/rust/Cargo.toml create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/client.rs create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/ffi.rs create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/lib.rs create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/server.rs create mode 100644 extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj create mode 100644 extensions/Bitwarden.OPAQUE/src/BitwardenException.cs create mode 100644 extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs create mode 100644 extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs create mode 100644 extensions/Bitwarden.OPAQUE/src/bitwarden.png create mode 100644 extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj create mode 100644 extensions/Bitwarden.OPAQUE/tests/Tests.cs diff --git a/extensions/Bitwarden.OPAQUE/rust/.gitignore b/extensions/Bitwarden.OPAQUE/rust/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/.gitignore @@ -0,0 +1 @@ +target diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.lock b/extensions/Bitwarden.OPAQUE/rust/Cargo.lock new file mode 100644 index 00000000..2c7dbce5 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/Cargo.lock @@ -0,0 +1,606 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[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 = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive-where" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" +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", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "once_cell" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" + +[[package]] +name = "opaque-ke" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb31f7b2c5760d8ffbc39652043f5cff14029b6249a2280b28ae6f0cd61f5098" +dependencies = [ + "argon2", + "curve25519-dalek", + "derive-where", + "digest", + "displaydoc", + "elliptic-curve", + "generic-array", + "getrandom", + "hkdf", + "hmac", + "rand", + "serde", + "subtle", + "voprf", + "zeroize", +] + +[[package]] +name = "opaque-ke-binding" +version = "0.0.0" +dependencies = [ + "argon2", + "digest", + "generic-array", + "opaque-ke", + "rand", + "voprf", + "zeroizing-alloc", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "voprf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f59c30c76e2fea54cdece6a054e2662feffa7ab19658a7887524265ee39470" +dependencies = [ + "curve25519-dalek", + "derive-where", + "digest", + "displaydoc", + "elliptic-curve", + "generic-array", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroizing-alloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebff5e6b81c1c7dca2d0bd333b2006da48cb37dbcae5a8da888f31fcb3c19934" diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml new file mode 100644 index 00000000..017bbee0 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "opaque-ke-binding" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["staticlib", "cdylib"] + +[dependencies] +argon2 = "0.5.3" +digest = "0.10.7" +generic-array = "0.14.7" +opaque-ke = { version = "3.0.0", features = ["std", "argon2", "curve25519"] } +rand = "0.8.5" +voprf = "0.5.0" +zeroizing-alloc = "0.1.0" diff --git a/extensions/Bitwarden.OPAQUE/rust/src/client.rs b/extensions/Bitwarden.OPAQUE/rust/src/client.rs new file mode 100644 index 00000000..2bcc320f --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/client.rs @@ -0,0 +1,67 @@ +use std::ops::Add; + +use digest::typenum::Sum; +use generic_array::ArrayLength; +use opaque_ke::{key_exchange::group::KeGroup, *}; +use rand::rngs::OsRng; +use voprf::Group; + +use crate::*; + +pub(crate) struct ClientRegistrationStartResult { + // The message is sent to the server for the next step of the registration protocol. + pub(crate) message: Vec, + // The state is stored temporarily by the client and used in the next step of the registration protocol. + pub(crate) state: Vec, +} + +pub(crate) fn start_client_registration( + password: &str, +) -> Result +where + as Group>::ScalarLen: Add< as Group>::ElemLen>, + ClientRegistrationLen: ArrayLength, +{ + let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; + + Ok(ClientRegistrationStartResult { + message: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) +} + +pub(crate) struct ClientRegistrationFinishResult { + // The message is sent to the server for the last step of the registration protocol. + pub(crate) message: Vec, + pub(crate) export_key: Vec, + pub(crate) server_s_pk: Vec, +} + +pub(crate) fn finish_client_registration( + state: &[u8], + registration_response_bytes: &[u8], + password: &str, +) -> Result +where + NonceLen: Add>>, + EnvelopeLen: ArrayLength, + ::PkLen: Add>>, + Sum<::PkLen, OutputSize>>: + ArrayLength + Add>, + RegistrationUploadLen: ArrayLength, +{ + let state = ClientRegistration::::deserialize(state)?; + + let result = state.finish( + &mut OsRng, + password.as_bytes(), + RegistrationResponse::deserialize(registration_response_bytes)?, + ClientRegistrationFinishParameters::default(), + )?; + + Ok(ClientRegistrationFinishResult { + message: result.message.serialize().to_vec(), + export_key: result.export_key.to_vec(), + server_s_pk: result.server_s_pk.serialize().to_vec(), + }) +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs new file mode 100644 index 00000000..ab1c553d --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs @@ -0,0 +1,121 @@ +use std::ffi::c_char; + +use crate::{DefaultCipherSuite, Error, ffi_types::*}; + +unsafe fn handle_string_input<'a>( + input: *const c_char, + name: &'static str, +) -> Result<&'a str, Response> { + let input = unsafe { std::ffi::CStr::from_ptr(input).to_str() }; + match input { + Ok(input) => Ok(input), + Err(_) => Err(Response::error(Error::InvalidInput(name))), + } +} + +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn start_server_registration( + request_bytes: Buffer, + username: *const c_char, +) -> Response { + let registration_request_bytes = unsafe { request_bytes.as_slice() }; + let username = match unsafe { handle_string_input(username, "username") } { + Ok(s) => s, + Err(e) => { + return e; + } + }; + + let response = match super::server::start_server_registration::( + registration_request_bytes, + username, + ) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok2(response.message, response.server_setup) +} + +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn finish_server_registration(registration_upload_bytes: Buffer) -> Response { + let registration_upload_bytes = unsafe { registration_upload_bytes.as_slice() }; + + let response = match super::server::finish_server_registration::( + registration_upload_bytes, + ) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok1(response) +} + +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> Response { + let password = match unsafe { handle_string_input(password, "password") } { + Ok(s) => s, + Err(e) => { + return e; + } + }; + + let result = match super::client::start_client_registration::(password) { + Ok(result) => result, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok2(result.message, result.state) +} + +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn finish_client_registration( + state_bytes: Buffer, + registration_response_bytes: Buffer, + password: *const c_char, +) -> Response { + let registration_response_bytes = unsafe { registration_response_bytes.as_slice() }; + let state_bytes = unsafe { state_bytes.as_slice() }; + + let password = match unsafe { handle_string_input(password, "password") } { + Ok(s) => s, + Err(e) => { + return e; + } + }; + + let response = match super::client::finish_client_registration::( + state_bytes, + registration_response_bytes, + password, + ) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok3(response.message, response.export_key, response.server_s_pk) +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs new file mode 100644 index 00000000..2975dab6 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs @@ -0,0 +1,115 @@ +use crate::Error; + +/// Free the buffer memory. +/// +/// # Safety +/// The parameter should contain a Buffer pointing to valid initialized memory, or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_buffer(buf: Buffer) { + buf.free(); +} + +#[repr(C)] +pub struct Buffer { + pub data: *mut u8, + pub len: usize, +} + +/// A struct to represent a buffer of data. +/// Important: The structure of this type must match the structure +/// of the Buffer type in the C# BitwardenLibrary, both in field type and order. +impl Buffer { + pub fn empty() -> Self { + Buffer { + data: std::ptr::null_mut(), + len: 0, + } + } + + pub fn from_vec(mut vec: Vec) -> Self { + // Important: Ensure that capacity and length are the same. + vec.shrink_to_fit(); + + let len = vec.len(); + let data = vec.as_mut_ptr(); + std::mem::forget(vec); + Buffer { data, len } + } + + pub unsafe fn as_slice(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.data, self.len) } + } + + pub fn free(self) { + if !self.data.is_null() { + let _ = unsafe { Vec::from_raw_parts(self.data, self.len, self.len) }; + } + } +} + +/// A struct to represent a response from the rust library. +/// Important: The structure of this type must match the structure +/// of the Response type in the C# BitwardenLibrary, both in field type and order. +#[repr(C)] +pub struct Response { + pub error: usize, + pub error_message: Buffer, + + // TODO: This is a way of returning multiple values without having different return FFI types. + // Ideally we'd have a separate type per return type? + pub data1: Buffer, + pub data2: Buffer, + pub data3: Buffer, +} + +impl Response { + pub fn ok1(data1: Vec) -> Self { + Response { + error: 0, + error_message: Buffer::empty(), + + data1: Buffer::from_vec(data1), + data2: Buffer::empty(), + data3: Buffer::empty(), + } + } + + pub fn ok2(data1: Vec, data2: Vec) -> Self { + Response { + error: 0, + error_message: Buffer::empty(), + + data1: Buffer::from_vec(data1), + data2: Buffer::from_vec(data2), + data3: Buffer::empty(), + } + } + + pub fn ok3(data1: Vec, data2: Vec, data3: Vec) -> Self { + Response { + error: 0, + error_message: Buffer::empty(), + + data1: Buffer::from_vec(data1), + data2: Buffer::from_vec(data2), + data3: Buffer::from_vec(data3), + } + } + + pub fn error(error: Error) -> Self { + // Important: The error codes need to be kept in sync with the BitwardenException in C#. + let (error, message) = match error { + Error::InvalidInput(name) => (1, name.to_string()), + Error::Protocol(e) => (2, format!("{:?}", e)), + }; + + Response { + error, + error_message: Buffer::from_vec(message.into_bytes()), + + data1: Buffer::empty(), + data2: Buffer::empty(), + data3: Buffer::empty(), + } + } +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs new file mode 100644 index 00000000..3667bd65 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs @@ -0,0 +1,86 @@ +#![forbid(unsafe_op_in_unsafe_fn)] + +use zeroizing_alloc::ZeroAlloc; + +#[global_allocator] +static ALLOC: ZeroAlloc = ZeroAlloc(std::alloc::System); + +mod client; +mod ffi; +mod ffi_types; +mod server; + +#[derive(Debug)] +pub enum Error { + InvalidInput(&'static str), + Protocol(opaque_ke::errors::ProtocolError), +} + +impl From for Error { + fn from(error: opaque_ke::errors::ProtocolError) -> Self { + Self::Protocol(error) + } +} + +// These type aliases were copied from the `opaque_ke` crate, as they are not public, but help with the type signatures. +pub(crate) type OutputSize = + <::Core as digest::OutputSizeUser>::OutputSize; +pub(crate) type OprfGroup = + <::OprfCs as voprf::CipherSuite>::Group; +pub(crate) type OprfHash = <::OprfCs as voprf::CipherSuite>::Hash; +type NonceLen = digest::consts::U32; +pub(crate) type EnvelopeLen = digest::typenum::Sum>>; +pub(crate) type ClientRegistrationLen = digest::typenum::Sum< + as voprf::Group>::ScalarLen, + as voprf::Group>::ElemLen, +>; +pub(crate) type ServerSetupLen = digest::typenum::Sum< + digest::typenum::Sum< + OutputSize>, + ::KeGroup>>::Len, + >, + <::KeGroup as opaque_ke::key_exchange::group::KeGroup>::SkLen, +>; + +pub struct DefaultCipherSuite; + +impl opaque_ke::CipherSuite for DefaultCipherSuite { + type OprfCs = opaque_ke::Ristretto255; + type KeGroup = opaque_ke::Ristretto255; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; + + type Ksf = argon2::Argon2<'static>; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + let password = "password"; + let username = "username"; + + let registration_request = + client::start_client_registration::(password).unwrap(); + + let server_start_result = server::start_server_registration::( + ®istration_request.message, + username, + ) + .unwrap(); + + let client_finish_result = client::finish_client_registration::( + ®istration_request.state, + &server_start_result.message, + password, + ) + .unwrap(); + + let server_finish_result = + server::finish_server_registration::(&client_finish_result.message) + .unwrap(); + + let _ = server_finish_result; + } +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/server.rs b/extensions/Bitwarden.OPAQUE/rust/src/server.rs new file mode 100644 index 00000000..769c6696 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/server.rs @@ -0,0 +1,59 @@ +use std::ops::Add; + +use digest::typenum::Sum; +use generic_array::ArrayLength; +use opaque_ke::{CipherSuite, key_exchange::group::KeGroup, keypair::PrivateKey, *}; +use rand::rngs::OsRng; +use voprf::Group; + +use crate::*; + +pub(crate) struct ServerRegistrationStartResult { + pub(crate) message: Vec, + pub(crate) server_setup: Vec, +} + +pub(crate) fn start_server_registration( + registration_request_bytes: &[u8], + username: &str, +) -> Result +where + // Message::Serialize + as Group>::ElemLen: Add<::PkLen>, + RegistrationResponseLen: ArrayLength, + // ServerSetup::Serialize + OutputSize>: Add<<::KeGroup as KeGroup>::SkLen>, + Sum>, <::KeGroup as KeGroup>::SkLen>: + ArrayLength + Add<::SkLen>, + ServerSetupLen>: ArrayLength, +{ + let server_setup = ServerSetup::::new(&mut OsRng); + + let result = ServerRegistration::start( + &server_setup, + RegistrationRequest::deserialize(registration_request_bytes)?, + username.as_bytes(), + )?; + + Ok(ServerRegistrationStartResult { + message: result.message.serialize().to_vec(), + server_setup: server_setup.serialize().to_vec(), + }) +} + +pub(crate) fn finish_server_registration( + registration_upload_bytes: &[u8], +) -> Result, Error> +where + NonceLen: Add>>, + EnvelopeLen: ArrayLength, + ::PkLen: Add>>, + Sum<::PkLen, OutputSize>>: + ArrayLength + Add>, + RegistrationUploadLen: ArrayLength, +{ + let registration = ServerRegistration::finish(RegistrationUpload::::deserialize( + registration_upload_bytes, + )?); + Ok(registration.serialize().to_vec()) +} diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj new file mode 100644 index 00000000..b4ddbe2f --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -0,0 +1,92 @@ + + + + net8.0 + true + enable + enable + + true + + + + Bitwarden Inc. + OPAQUE-KE bindings for .NET and C# by Bitwarden + + https://github.com/bitwarden/dotnet-extensions + https://github.com/bitwarden/dotnet-extensions/releases + GPL-3.0-only + false + README.md + true + true + true + + Bitwarden.OPAQUE + Bitwarden OPAQUE-KE library + Bitwarden Inc. + OPAQUE + + Bitwarden.OPAQUE + bitwarden.png + Bitwarden;OPAQUE;PAKE;.NET + 1.0.0 + + + + + + + + + + + Always + true + + + Always + true + + + Always + true + + + + + + Always + true + runtimes/osx-x64/native + + + Always + true + runtimes/osx-arm64/native + + + Always + true + runtimes/linux-x64/native + + + Always + true + runtimes/linux-arm64/native + + + Always + true + runtimes/win-x64/native + + + Always + true + runtimes/win-arm64/native + + + \ No newline at end of file diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs new file mode 100644 index 00000000..b6931a97 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs @@ -0,0 +1,19 @@ +namespace Bitwarden.OPAQUE; + +public class BitwardenException(int errorCode, string message) : Exception($"Error {getCodeName(errorCode)} - {message}") +{ + private static string getCodeName(int code) + { + // Important: This needs to be kept in sync with the error codes in the rust library. + return code switch + { + 0 => "OK", + 1 => "INVALID_INPUT", + 2 => "PROTOCOL_ERROR", + + // This is a special case and it's only used in the C# code. + 100 => "UNEXPECTED_RETURN", + _ => "UNKNOWN", + }; + } +} diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs new file mode 100644 index 00000000..02aad2fa --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -0,0 +1,158 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Bitwarden.OPAQUE; + +internal static partial class BitwardenLibrary +{ + + /// + /// A struct to represent a buffer of data. + /// Important: The structure of this type must match the structure + /// of the Buffer type in the rust crate, both in field type and order. + /// + [StructLayout(LayoutKind.Sequential)] + private struct Buffer + { + public IntPtr data; + public nint size; + } + + /// + /// A struct to represent a response from the rust library. + /// Important: The structure of this type must match the structure + /// of the Response type in the rust crate, both in field type and order. + /// + [StructLayout(LayoutKind.Sequential)] + private struct Response + { + public nint error; + public Buffer error_message; + + public Buffer data1; + public Buffer data2; + public Buffer data3; + } + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial void free_buffer(Buffer buf); + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response start_server_registration(Buffer request_bytes, string username); + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response finish_server_registration(Buffer registration_upload_bytes); + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response start_client_registration(string password); + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response finish_client_registration(Buffer state_bytes, Buffer registration_response_bytes, string password); + + + private static Buffer BuildBuffer(byte[] data, out GCHandle handle) + { + handle = GCHandle.Alloc(data, GCHandleType.Pinned); + return new Buffer + { + data = handle.AddrOfPinnedObject(), + size = data.Length + }; + } + + private static byte[]? CopyAndFreeBuffer(Buffer buffer) + { + if (buffer.data == IntPtr.Zero) return null; + if (buffer.size == 0) return []; + + var data = new byte[buffer.size]; + Marshal.Copy(buffer.data, data, 0, (int)buffer.size); + free_buffer(buffer); + return data; + } + + private static List HandleResponse(Response response, int expectedValues) + { + // If we receive an error, parse the message and throw an exception + if (response.error != 0) + { + var message = CopyAndFreeBuffer(response.error_message); + string messageStr; + try { messageStr = Encoding.UTF8.GetString(message!); } catch { messageStr = ""; } + throw new BitwardenException((int)response.error, messageStr); + } + + // If we don't receive an error, parse all the return types + var buffers = new Buffer[] { response.data1, response.data2, response.data3 }; + var arrays = new List { }; + + foreach (var buffer in buffers) + { + var data = CopyAndFreeBuffer(buffer); + if (data == null) break; + arrays.Add(data); + } + if (arrays.Count != expectedValues) + { + throw new BitwardenException(100, $"Invalid number of return values. Expected {expectedValues}, got {arrays.Count}"); + } + + return arrays; + } + + internal static (byte[], byte[]) StartServerRegistration(byte[] requestBytes, string username) + { + var requestBuffer = BuildBuffer(requestBytes, out var handle); + try + { + var response = start_server_registration(requestBuffer, username); + var ret = HandleResponse(response, 2); + return (ret[0], ret[1]); + } + finally + { + handle.Free(); + } + + } + + internal static byte[] FinishServerRegistration(byte[] registrationUploadBytes) + { + var registrationUploadBuffer = BuildBuffer(registrationUploadBytes, out var handle); + try + { + var response = finish_server_registration(registrationUploadBuffer); + return HandleResponse(response, 1)[0]; + } + finally + { + handle.Free(); + } + + } + + internal static (byte[], byte[]) StartClientRegistration(string password) + { + var response = start_client_registration(password); + var ret = HandleResponse(response, 2); + return (ret[0], ret[1]); + } + + internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] stateBytes, byte[] registrationResponseBytes, string password) + { + var stateBuffer = BuildBuffer(stateBytes, out var stateHandle); + var registrationResponseBuffer = BuildBuffer(registrationResponseBytes, out var registrationResponseHandle); + + try + { + var response = finish_client_registration(stateBuffer, registrationResponseBuffer, password); + var ret = HandleResponse(response, 3); + return (ret[0], ret[1], ret[2]); + } + finally + { + stateHandle.Free(); + registrationResponseHandle.Free(); + } + } +} diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs new file mode 100644 index 00000000..37ac2381 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs @@ -0,0 +1,62 @@ +namespace Bitwarden.OPAQUE; + +public enum OprfCS +{ + Ristretto255 +} + +public enum KeGroup +{ + Ristretto255 +} + +public enum KeyExchange +{ + TripleDH +} + +public abstract record KSF; + +public record Argon2id(int iterations, int memoryKiB, int parallelism) : KSF; + +public struct CipherConfiguration +{ + public OprfCS OprfCS; + public KeGroup KeGroup; + public KeyExchange KeyExchange; + public KSF KSF; + + public static readonly CipherConfiguration Default = new CipherConfiguration + { + OprfCS = OprfCS.Ristretto255, + KeGroup = KeGroup.Ristretto255, + KeyExchange = KeyExchange.TripleDH, + KSF = new Argon2id(4, 65536, 4) + }; + +} + +public sealed partial class BitwardenOpaque +{ + + + public (byte[], byte[]) StartServerRegistration(CipherConfiguration config, byte[] requestBytes, string username) + { + return BitwardenLibrary.StartServerRegistration(requestBytes, username); + } + + public byte[] FinishServerRegistration(CipherConfiguration config, byte[] registrationUploadBytes) + { + return BitwardenLibrary.FinishServerRegistration(registrationUploadBytes); + } + + public (byte[], byte[]) StartClientRegistration(CipherConfiguration config, string password) + { + return BitwardenLibrary.StartClientRegistration(password); + } + + public (byte[], byte[], byte[]) FinishClientRegistration(CipherConfiguration config, byte[] stateBytes, byte[] registrationResponseBytes, string password) + { + return BitwardenLibrary.FinishClientRegistration(stateBytes, registrationResponseBytes, password); + } +} diff --git a/extensions/Bitwarden.OPAQUE/src/bitwarden.png b/extensions/Bitwarden.OPAQUE/src/bitwarden.png new file mode 100644 index 0000000000000000000000000000000000000000..681629a2716a6c9fff49281850bbc4ec34cf43cf GIT binary patch literal 790 zcmV+x1L^#UP)C00012P)t-s0000W zVBZ*E-y2}x7+~KRVB8m9-56cn7hT*KUECL5-4|Zn|NsB-_5I@O`_$q1#n<+?%=D~f{r0Z^)>0prOQHJG1d*d{5;3j9@7hT-w**Kg4000bhQchC<5Fju>V2_`# zzt8W9O#{6E00Lr3L_t(|+U?uTlBzHehGBFDQA~pJ6M=m9f1PJ>rnIm@D(OI6^t-q6 zOk=F6(#|Fw^P~V74;Dd`3miV7?`Qik<)jAb_m^?Z3~)GO-=71h6A7jXuJUf8{iCi0zLp#TtKT{RqzK`27rvv1^{3}0rUYFga9lHa3=%s z2EZ@?ts(>P2iPzGeW?uu;1&X)=id+t7(xK_bwe-!tv`VN1gXV3fbnUx`qrty0XXdk zpyzAH0$}h3)N0xQ9st%0;NCBe1s8zM127mhtpgu`dJV8_t-$~g!5YBb>G=!*5v&0o znw}pEfNmAw8uea>cmUWHz{9BbEaCv@fGJ?StM~K+aRC_gBB0gu`~k56aMe7(8bzuc z7y!S^a8tAzU;_&9`Wz}A4*t)uA{*c$Nq+@ak^ z>;nLLqe!L)b^`#NR+DrG>R literal 0 HcmV?d00001 diff --git a/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj b/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj new file mode 100644 index 00000000..d9de6845 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs new file mode 100644 index 00000000..189618ff --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -0,0 +1,33 @@ +namespace Bitwarden.OPAQUE.Tests; + +using Xunit; + +public class SampleTests +{ + [Fact] + public void RunSample_Works() + { + // Get environment variables + var username = "demo_username"; + var password = "demo_password"; + + // Create the OPAQUE Client + var client = new BitwardenOpaque(); + + var config = CipherConfiguration.Default; + + // Start the client registration + var (clientRequest, clientState) = client.StartClientRegistration(config, password); + + // Client sends reg_start to server + var (serverResponse, serverSetup) = client.StartServerRegistration(config, clientRequest, username); + + // Server sends server_start_result to client + var (registrationUpload, exportKey, serverSPKey) = client.FinishClientRegistration(config, clientState, serverResponse, password); + + // Client sends client_finish_result to server + var result = client.FinishServerRegistration(config, registrationUpload); + + Assert.NotNull(result); + } +} From fd6fc5da833cfa5cb5e701a7bff9f8084fba7845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 16:28:09 +0100 Subject: [PATCH 02/89] Add project to SLN --- bitwarden-dotnet.sln | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bitwarden-dotnet.sln b/bitwarden-dotnet.sln index 7e7e4498..9f66a986 100644 --- a/bitwarden-dotnet.sln +++ b/bitwarden-dotnet.sln @@ -45,6 +45,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4949B721 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Server.Sdk.Features.Tests", "extensions\Bitwarden.Server.Sdk.Features\tests\Bitwarden.Server.Sdk.Features.Tests\Bitwarden.Server.Sdk.Features.Tests.csproj", "{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bitwarden.OPAQUE", "Bitwarden.OPAQUE", "{023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.OPAQUE", "extensions\Bitwarden.OPAQUE\src\Bitwarden.OPAQUE.csproj", "{B49E33DF-A672-4361-BECA-C3DA423BD7A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +106,10 @@ Global {1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU {1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Release|Any CPU.Build.0 = Release|Any CPU + {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5EC8B943-2E9E-437D-9FFC-D18B5DB4D7D0} = {695C76EF-1102-4805-970F-7C995EE54930} @@ -124,5 +132,7 @@ Global {DF914CD1-F916-4A58-B749-625DB67FAAA7} = {026589E0-5AAA-44EB-B973-3CFFF5B54AFC} {4949B721-5C7F-4D85-AB35-F57B54D7A6E6} = {026589E0-5AAA-44EB-B973-3CFFF5B54AFC} {1789F567-87B3-4313-80CF-E3CCFA1B6D5E} = {4949B721-5C7F-4D85-AB35-F57B54D7A6E6} + {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} = {695C76EF-1102-4805-970F-7C995EE54930} + {B49E33DF-A672-4361-BECA-C3DA423BD7A9} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} EndGlobalSection EndGlobal From 450b5e0e18725e14a69ce3b9652154bb25c6ad72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 16:32:47 +0100 Subject: [PATCH 03/89] Also load Opaque tests --- bitwarden-dotnet.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bitwarden-dotnet.sln b/bitwarden-dotnet.sln index 9f66a986..d8663c91 100644 --- a/bitwarden-dotnet.sln +++ b/bitwarden-dotnet.sln @@ -49,6 +49,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bitwarden.OPAQUE", "Bitward EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.OPAQUE", "extensions\Bitwarden.OPAQUE\src\Bitwarden.OPAQUE.csproj", "{B49E33DF-A672-4361-BECA-C3DA423BD7A9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.OPAQUE.Tests", "extensions\Bitwarden.OPAQUE\tests\Bitwarden.OPAQUE.Tests.csproj", "{DC9DAB81-5ED7-4756-BF20-D594D87D2865}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +112,10 @@ Global {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Release|Any CPU.Build.0 = Release|Any CPU + {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5EC8B943-2E9E-437D-9FFC-D18B5DB4D7D0} = {695C76EF-1102-4805-970F-7C995EE54930} @@ -134,5 +140,6 @@ Global {1789F567-87B3-4313-80CF-E3CCFA1B6D5E} = {4949B721-5C7F-4D85-AB35-F57B54D7A6E6} {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} = {695C76EF-1102-4805-970F-7C995EE54930} {B49E33DF-A672-4361-BECA-C3DA423BD7A9} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} + {DC9DAB81-5ED7-4756-BF20-D594D87D2865} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} EndGlobalSection EndGlobal From 9a5b8f61f432001461f78bd6f5c825352d2993d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 16:41:38 +0100 Subject: [PATCH 04/89] Build Rust on csproj and CI --- .github/workflows/build.yml | 5 +++++ .github/workflows/pack-and-release.yml | 5 +++++ .github/workflows/scan.yml | 5 +++++ .github/workflows/test.yml | 5 +++++ extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 583b91d0..bacfba55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,5 +19,10 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + - name: Install rust + uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable + with: + toolchain: stable + - name: Build solution run: dotnet build diff --git a/.github/workflows/pack-and-release.yml b/.github/workflows/pack-and-release.yml index 649814b6..41a1c567 100644 --- a/.github/workflows/pack-and-release.yml +++ b/.github/workflows/pack-and-release.yml @@ -23,6 +23,11 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + - name: Install rust + uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable + with: + toolchain: stable + - name: Parse package id: parse-package uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index e0cd62a9..9277cff0 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -74,6 +74,11 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + - name: Install rust + uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable + with: + toolchain: stable + - name: Install SonarCloud scanner run: dotnet tool install dotnet-sonarscanner -g diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c926768c..f903e5e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,11 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + - name: Install rust + uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable + with: + toolchain: stable + - name: Test solution run: dotnet test --configuration Debug --logger "trx;LogFileName=test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index b4ddbe2f..6dc534e9 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -33,6 +33,11 @@ 1.0.0 + + + + From 8ab505c9224b4d98a4b1efcfdd0103f5db2475b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 16:51:31 +0100 Subject: [PATCH 05/89] Debug file --- extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index 6dc534e9..9821c1e8 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -36,6 +36,9 @@ + + From cf7e77a03e043ca31a31a077e16aa2d6974e5c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 17:12:33 +0100 Subject: [PATCH 06/89] Remove debug file --- extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index 9821c1e8..6dc534e9 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -36,9 +36,6 @@ - - From c15ffe712b579dc1cd1b5794f08dcc1296d20a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 18:34:26 +0100 Subject: [PATCH 07/89] Some small build improvements --- extensions/Bitwarden.OPAQUE/rust/.gitignore | 1 + .../src/Bitwarden.OPAQUE.csproj | 51 ++++++++++++------- extensions/Bitwarden.OPAQUE/src/README.md | 3 ++ 3 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 extensions/Bitwarden.OPAQUE/src/README.md diff --git a/extensions/Bitwarden.OPAQUE/rust/.gitignore b/extensions/Bitwarden.OPAQUE/rust/.gitignore index eb5a316c..580f45e9 100644 --- a/extensions/Bitwarden.OPAQUE/rust/.gitignore +++ b/extensions/Bitwarden.OPAQUE/rust/.gitignore @@ -1 +1,2 @@ target +dist diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index 6dc534e9..19c37f8e 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -33,62 +33,75 @@ 1.0.0 - - - - - - + + + + ../rust/target/debug/ + + + + + + + - + Always true - + + Always true - + + Always true + - + Always true runtimes/osx-x64/native - + + Always true runtimes/osx-arm64/native - + + Always true runtimes/linux-x64/native - + + Always true runtimes/linux-arm64/native - + + Always true runtimes/win-x64/native - + + Always true runtimes/win-arm64/native diff --git a/extensions/Bitwarden.OPAQUE/src/README.md b/extensions/Bitwarden.OPAQUE/src/README.md new file mode 100644 index 00000000..c70f97ec --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/README.md @@ -0,0 +1,3 @@ +# Bitwarden OPAQUE-KE library + +OPAQUE-KE bindings for .NET and C# by Bitwarden From 2643fd6ad9f73fba3b369003a367932319860e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:18:17 +0100 Subject: [PATCH 08/89] Test cross compile in CI --- .../workflows/build-rust-cross-platform.yml | 81 +++++++++++++++++++ .github/workflows/build.yml | 48 +++++++++++ .../src/Bitwarden.OPAQUE.csproj | 11 +-- 3 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build-rust-cross-platform.yml diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml new file mode 100644 index 00000000..96163121 --- /dev/null +++ b/.github/workflows/build-rust-cross-platform.yml @@ -0,0 +1,81 @@ +name: Build Rust Cross Platform + +on: + workflow_call: + workflow_dispatch: + push: + branches: + - main + - rc + - hotfix-rc + pull_request: + +jobs: + build_rust: + name: Build for ${{ matrix.settings.os }} ${{ matrix.settings.target }} + runs-on: ${{ matrix.settings.os }} + strategy: + fail-fast: false + matrix: + settings: + - os: macos-13 + target: x86_64-apple-darwin + - os: macos-13 + target: aarch64-apple-darwin + - os: windows-2022 + target: x86_64-pc-windows-msvc + # caution: updating the linux runner OS version for GNU + # targets will likely break the library for older OS versions. + # prefer using oldest supported runner for for these targets + - os: ubuntu-22.04 + target: x86_64-unknown-linux-musl + - os: ubuntu-22.04 + target: aarch64-unknown-linux-musl + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install rust + uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 + + - name: Install Zig + if: ${{ contains(matrix.settings.target, 'musl') }} + uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 + with: + version: 0.12.0 + + - name: Install Zigbuild + if: ${{ contains(matrix.settings.target, 'musl') }} + run: cargo install cargo-zigbuild --locked --git https://github.com/rust-cross/cargo-zigbuild --rev 6f7e1336c9cd13cf1b3704f93c40fcf84caaed6b # 0.18.4 + + - name: Add build architecture + run: rustup target add ${{ matrix.settings.target }} + + # Build Rust for musl + - name: Build Rust for - ${{ matrix.settings.target }} + if: ${{ contains(matrix.settings.target, 'musl') }} + env: + RUSTFLAGS: "-D warnings" + run: cargo zigbuild --target ${{ matrix.settings.target }} --release + working-directory: extensions/Bitwarden.OPAQUE/rust + + # Build Rust for !musl + - name: Build Rust for - ${{ matrix.settings.target }} + if: ${{ !contains(matrix.settings.target, 'musl') }} + env: + RUSTFLAGS: "-D warnings" + MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems + run: cargo build --target ${{ matrix.settings.target }} --release + working-directory: extensions/Bitwarden.OPAQUE/rust + + - name: Upload Artifact + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: libopaque_ke_binding_files-${{ matrix.settings.target }} + path: target/${{ matrix.settings.target }}/release/*libopaque_ke_binding* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bacfba55..bb83e847 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,14 @@ on: pull_request: jobs: + build_rust: + name: Build Rust + uses: ./.github/workflows/build-rust-cross-platform.yml + build-artifacts: name: Build artifacts + needs: + - build_rust runs-on: ubuntu-22.04 steps: @@ -26,3 +32,45 @@ jobs: - name: Build solution run: dotnet build + + # TODO: Testing OPAQUE building, remove this when done and add it to pack-and-release.yml? + + - name: Download x86_64-apple-darwin files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-x86_64-apple-darwin + path: extensions/Bitwarden.OPAQUE/rust/dist/osx-x64 + + - name: Download aarch64-apple-darwin files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libbitwarden_c_files-aarch64-apple-darwin + path: extensions/Bitwarden.OPAQUE/rust/dist/osx-arm64 + + - name: Download x86_64-pc-windows-msvc files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libbitwarden_c_files-x86_64-pc-windows-msvc + path: extensions/Bitwarden.OPAQUE/rust/dist/win-x64 + + - name: Download x86_64-unknown-linux-musl files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libbitwarden_c_files-x86_64-unknown-linux-musl + path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 + + - name: Download aarch64-unknown-linux-musl files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libbitwarden_c_files-aarch64-unknown-linux-musl + path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 + + - name: Pack solution + run: dotnet pack -c Release --output ./nuget-output + + - name: Upload NuGet package + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: Bitwarden.OPAQUE.nupkg + path: | + ./nuget-output/*.nupkg diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index 19c37f8e..b419650b 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -45,8 +45,7 @@ - + @@ -71,13 +70,13 @@ - + Always true runtimes/osx-x64/native - + Always true runtimes/osx-arm64/native @@ -95,16 +94,18 @@ runtimes/linux-arm64/native - + Always true runtimes/win-x64/native + \ No newline at end of file From 053bcc401b8912a3e87fa74a53b417d88e77f5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:19:35 +0100 Subject: [PATCH 09/89] Temp fix tests --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f903e5e2..9f175b9d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: pull_request: jobs: - testing: name: Run tests runs-on: ubuntu-22.04 @@ -29,6 +28,11 @@ jobs: with: toolchain: stable + # This is needed because MSBuild is trying to copy the native library before it's built. + # TODO: Improve the build process so this isn't needed. + - name: Run build to ensure that the native modules are built + run: dotnet build + - name: Test solution run: dotnet test --configuration Debug --logger "trx;LogFileName=test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" From 691c9a7643c25243cbd96317cb7951fe8c76a649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:23:27 +0100 Subject: [PATCH 10/89] Fix wrong path and ignore first build error --- .github/workflows/build-rust-cross-platform.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 96163121..92345d15 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -78,4 +78,4 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: target/${{ matrix.settings.target }}/release/*libopaque_ke_binding* + path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*libopaque_ke_binding* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f175b9d..79e09faf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: # This is needed because MSBuild is trying to copy the native library before it's built. # TODO: Improve the build process so this isn't needed. - name: Run build to ensure that the native modules are built - run: dotnet build + run: dotnet build || true - name: Test solution run: dotnet test --configuration Debug --logger "trx;LogFileName=test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" From d3be32b22c2d0fcc316d83ad9b41474d80469333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:25:20 +0100 Subject: [PATCH 11/89] Enable LTO --- extensions/Bitwarden.OPAQUE/rust/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml index 017bbee0..db76aef9 100644 --- a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml +++ b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml @@ -15,3 +15,8 @@ opaque-ke = { version = "3.0.0", features = ["std", "argon2", "curve25519"] } rand = "0.8.5" voprf = "0.5.0" zeroizing-alloc = "0.1.0" + +# Turn on LTO on release mode +[profile.release] +lto = "thin" +codegen-units = 1 From b7663619844c7a0ff8d232aa28e22960d2aa1e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:28:45 +0100 Subject: [PATCH 12/89] Force build module directly --- .github/workflows/build.yml | 10 +++++++++- .github/workflows/test.yml | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb83e847..53716e21 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,15 @@ jobs: with: toolchain: stable - - name: Build solution + # This is needed because MSBuild is trying to copy the native library before it's built. + # TODO: Improve the build process so this isn't needed. + - name: Build native module in debug + run: | + cargo build + ls -la target/debug + working-directory: extensions/Bitwarden.OPAQUE/rust + + - name: Build solution in debug run: dotnet build # TODO: Testing OPAQUE building, remove this when done and add it to pack-and-release.yml? diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79e09faf..d8d0250a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,8 +30,11 @@ jobs: # This is needed because MSBuild is trying to copy the native library before it's built. # TODO: Improve the build process so this isn't needed. - - name: Run build to ensure that the native modules are built - run: dotnet build || true + - name: Build native module in debug + run: | + cargo build + ls -la target/debug + working-directory: extensions/Bitwarden.OPAQUE/rust - name: Test solution run: dotnet test --configuration Debug --logger "trx;LogFileName=test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" From d3689e9a40fdd752148c5194ac757b8c570fef92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:32:33 +0100 Subject: [PATCH 13/89] Rename invalid artifact names --- .github/workflows/build-rust-cross-platform.yml | 2 +- .github/workflows/build.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 92345d15..d09a7429 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -78,4 +78,4 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*libopaque_ke_binding* + path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53716e21..c525019d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,25 +52,25 @@ jobs: - name: Download aarch64-apple-darwin files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libbitwarden_c_files-aarch64-apple-darwin + name: libopaque_ke_binding_files-aarch64-apple-darwin path: extensions/Bitwarden.OPAQUE/rust/dist/osx-arm64 - name: Download x86_64-pc-windows-msvc files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libbitwarden_c_files-x86_64-pc-windows-msvc + name: libopaque_ke_binding_files-x86_64-pc-windows-msvc path: extensions/Bitwarden.OPAQUE/rust/dist/win-x64 - name: Download x86_64-unknown-linux-musl files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libbitwarden_c_files-x86_64-unknown-linux-musl + name: libopaque_ke_binding_files-x86_64-unknown-linux-musl path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 - name: Download aarch64-unknown-linux-musl files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libbitwarden_c_files-aarch64-unknown-linux-musl + name: libopaque_ke_binding_files-aarch64-unknown-linux-musl path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 - name: Pack solution From 2bce8e445ed6feacff81bbeea74f174f6d86c652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:42:07 +0100 Subject: [PATCH 14/89] Remove zig --- .../workflows/build-rust-cross-platform.yml | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index d09a7429..621630c5 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -44,30 +44,11 @@ jobs: - name: Cache cargo registry uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 - - name: Install Zig - if: ${{ contains(matrix.settings.target, 'musl') }} - uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 - with: - version: 0.12.0 - - - name: Install Zigbuild - if: ${{ contains(matrix.settings.target, 'musl') }} - run: cargo install cargo-zigbuild --locked --git https://github.com/rust-cross/cargo-zigbuild --rev 6f7e1336c9cd13cf1b3704f93c40fcf84caaed6b # 0.18.4 - - name: Add build architecture run: rustup target add ${{ matrix.settings.target }} - # Build Rust for musl - - name: Build Rust for - ${{ matrix.settings.target }} - if: ${{ contains(matrix.settings.target, 'musl') }} - env: - RUSTFLAGS: "-D warnings" - run: cargo zigbuild --target ${{ matrix.settings.target }} --release - working-directory: extensions/Bitwarden.OPAQUE/rust - - # Build Rust for !musl + # Build Rust - name: Build Rust for - ${{ matrix.settings.target }} - if: ${{ !contains(matrix.settings.target, 'musl') }} env: RUSTFLAGS: "-D warnings" MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems From c9524bd81480f52b80aea872b68d13ec6e0d67cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 19:43:01 +0100 Subject: [PATCH 15/89] Just build dynlibs --- .github/workflows/build-rust-cross-platform.yml | 4 ++-- .github/workflows/build.yml | 8 ++++---- extensions/Bitwarden.OPAQUE/rust/Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 621630c5..483d9b35 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -28,9 +28,9 @@ jobs: # targets will likely break the library for older OS versions. # prefer using oldest supported runner for for these targets - os: ubuntu-22.04 - target: x86_64-unknown-linux-musl + target: x86_64-unknown-linux-gnu - os: ubuntu-22.04 - target: aarch64-unknown-linux-musl + target: aarch64-unknown-linux-gnu steps: - name: Checkout diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c525019d..67677a63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,16 +61,16 @@ jobs: name: libopaque_ke_binding_files-x86_64-pc-windows-msvc path: extensions/Bitwarden.OPAQUE/rust/dist/win-x64 - - name: Download x86_64-unknown-linux-musl files + - name: Download x86_64-unknown-linux-gnu files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-x86_64-unknown-linux-musl + name: libopaque_ke_binding_files-x86_64-unknown-linux-gnu path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 - - name: Download aarch64-unknown-linux-musl files + - name: Download aarch64-unknown-linux-gnu files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-aarch64-unknown-linux-musl + name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 - name: Pack solution diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml index db76aef9..4e55f27d 100644 --- a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml +++ b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" publish = false [lib] -crate-type = ["staticlib", "cdylib"] +crate-type = ["cdylib"] [dependencies] argon2 = "0.5.3" From 2888c3b7068dd0eb861302f4fefce6e458b58bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:03:29 +0100 Subject: [PATCH 16/89] Try cross --- .github/workflows/build-rust-cross-platform.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 483d9b35..83bde0b4 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -44,17 +44,30 @@ jobs: - name: Cache cargo registry uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5 + - name: Install cross + if: ${{ startsWith(matrix.settings.os, 'ubuntu') && !startsWith(matrix.settings.target, 'x86_64') }} + run: cargo install cross --git https://github.com/cross-rs/cross --rev 36c0d7810ddde073f603c82d896c2a6c886ff7a4 + - name: Add build architecture run: rustup target add ${{ matrix.settings.target }} - # Build Rust - - name: Build Rust for - ${{ matrix.settings.target }} + # Build Rust natively + - name: Build Rust native for - ${{ matrix.settings.target }} + if: ${{ !startsWith(matrix.settings.os, 'ubuntu') || startsWith(matrix.settings.target, 'x86_64') }} env: RUSTFLAGS: "-D warnings" MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems run: cargo build --target ${{ matrix.settings.target }} --release working-directory: extensions/Bitwarden.OPAQUE/rust + # Build Rust using cross + - name: Build Rust cross for - ${{ matrix.settings.target }} + if: ${{ startsWith(matrix.settings.os, 'ubuntu') && !startsWith(matrix.settings.target, 'x86_64') }} + env: + RUSTFLAGS: "-D warnings" + run: cross build --target ${{ matrix.settings.target }} --release + working-directory: extensions/Bitwarden.OPAQUE/rust + - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: From a40959bc639d274a5bba674fb267cbdd43ded5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:11:37 +0100 Subject: [PATCH 17/89] Build ARM and fix linux arm64 path --- .github/workflows/build-rust-cross-platform.yml | 2 ++ .github/workflows/build.yml | 8 +++++++- extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 83bde0b4..84d3cec5 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -31,6 +31,8 @@ jobs: target: x86_64-unknown-linux-gnu - os: ubuntu-22.04 target: aarch64-unknown-linux-gnu + - os: ubuntu-22.04 + target: arm-unknown-linux-gnueabihf steps: - name: Checkout diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67677a63..c90526c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,7 +71,13 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu - path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 + path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm64 + + - name: Download arm-unknown-linux-gnueabihf files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf + path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm - name: Pack solution run: dotnet pack -c Release --output ./nuget-output diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index b419650b..f1afff62 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -94,6 +94,12 @@ runtimes/linux-arm64/native + + Always + true + runtimes/linux-arm/native + + Always true From 6a8d8b66ec2674b56e02f94fd9b0c2a74a27e15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:15:37 +0100 Subject: [PATCH 18/89] Only run cross platform from other workflows --- .github/workflows/build-rust-cross-platform.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 84d3cec5..8d78e1eb 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -2,13 +2,6 @@ name: Build Rust Cross Platform on: workflow_call: - workflow_dispatch: - push: - branches: - - main - - rc - - hotfix-rc - pull_request: jobs: build_rust: From d2aa8648999e538f823aabb3a77acc57b0d42b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:16:47 +0100 Subject: [PATCH 19/89] Only upload opaque --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c90526c9..89757758 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,4 +87,4 @@ jobs: with: name: Bitwarden.OPAQUE.nupkg path: | - ./nuget-output/*.nupkg + ./nuget-output/Bitwarden.OPAQUE*.nupkg From 049e9bf7ec493f129586876ed25094bda5e46f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:26:30 +0100 Subject: [PATCH 20/89] More ARM builds --- .github/workflows/build-rust-cross-platform.yml | 6 +++++- .github/workflows/build.yml | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 8d78e1eb..0e3f3a89 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -25,7 +25,11 @@ jobs: - os: ubuntu-22.04 target: aarch64-unknown-linux-gnu - os: ubuntu-22.04 - target: arm-unknown-linux-gnueabihf + target: armv7-unknown-linux-gnueabihf + - os: ubuntu-22.04 + target: armv6-unknown-linux-gnueabihf + - os: ubuntu-22.04 + target: armv5te-unknown-linux-gnueabi steps: - name: Checkout diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89757758..81ed5486 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,12 +73,24 @@ jobs: name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm64 - - name: Download arm-unknown-linux-gnueabihf files + - name: Download armv7-unknown-linux-gnueabihf files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf + name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm + - name: Download armv6-unknown-linux-gnueabihf files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-armv6-unknown-linux-gnueabihf + path: extensions/Bitwarden.OPAQUE/rust/dist/linux-armv6 + + - name: Download armv5te-unknown-linux-gnueabi files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-armv5te-unknown-linux-gnueabi + path: extensions/Bitwarden.OPAQUE/rust/dist/linux-armel + - name: Pack solution run: dotnet pack -c Release --output ./nuget-output From cee3ba8af38dab9c1572589dfe530fae8c2037a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:28:57 +0100 Subject: [PATCH 21/89] Ignore missing files --- .github/workflows/test.yml | 1 + extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8d0250a..fe0e116e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ on: pull_request: jobs: + testing: name: Run tests runs-on: ubuntu-22.04 diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index f1afff62..16b48388 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -50,19 +50,19 @@ + Condition="Exists('$(RustDebug)/libopaque_ke_binding.dylib')"> Always true + Condition="Exists('$(RustDebug)/libopaque_ke_binding.so')"> Always true + Condition="Exists('$(RustDebug)/opaque_ke_binding.dll')"> Always true From dd3cb3132e537ff4f5e44037069d8bcb17ae1f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 11 Mar 2025 20:32:52 +0100 Subject: [PATCH 22/89] Fix armv6 --- .github/workflows/build-rust-cross-platform.yml | 2 +- .github/workflows/build.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 0e3f3a89..33df5681 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -27,7 +27,7 @@ jobs: - os: ubuntu-22.04 target: armv7-unknown-linux-gnueabihf - os: ubuntu-22.04 - target: armv6-unknown-linux-gnueabihf + target: arm-unknown-linux-gnueabihf - os: ubuntu-22.04 target: armv5te-unknown-linux-gnueabi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81ed5486..1b1c5382 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,10 +79,10 @@ jobs: name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm - - name: Download armv6-unknown-linux-gnueabihf files + - name: Download arm-unknown-linux-gnueabihf files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-armv6-unknown-linux-gnueabihf + name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf path: extensions/Bitwarden.OPAQUE/rust/dist/linux-armv6 - name: Download armv5te-unknown-linux-gnueabi files From bacd05c45f5b6af644074f712f4e5cdb94aebfd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 11:19:17 +0100 Subject: [PATCH 23/89] Fix dotnet test not building binaries --- .github/workflows/test.yml | 8 ---- .../src/Bitwarden.OPAQUE.csproj | 43 ++++++++++--------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe0e116e..f903e5e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,14 +29,6 @@ jobs: with: toolchain: stable - # This is needed because MSBuild is trying to copy the native library before it's built. - # TODO: Improve the build process so this isn't needed. - - name: Build native module in debug - run: | - cargo build - ls -la target/debug - working-directory: extensions/Bitwarden.OPAQUE/rust - - name: Test solution run: dotnet test --configuration Debug --logger "trx;LogFileName=test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index 16b48388..f065fa89 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -39,34 +39,37 @@ + + ../rust/target/debug/ - - + + opaque_ke_binding.dll + + + libopaque_ke_binding.so + + + libopaque_ke_binding.dylib + + + - - - Always - true - - - - Always - true - + + + + Always + true + + + - - Always - true - - From c12061dfbdaf8a572a309edc68927641fda7bc8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 11:26:33 +0100 Subject: [PATCH 24/89] Remove workaround --- .github/workflows/build.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b1c5382..3d6b5b08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,15 +30,7 @@ jobs: with: toolchain: stable - # This is needed because MSBuild is trying to copy the native library before it's built. - # TODO: Improve the build process so this isn't needed. - - name: Build native module in debug - run: | - cargo build - ls -la target/debug - working-directory: extensions/Bitwarden.OPAQUE/rust - - - name: Build solution in debug + - name: Build solution run: dotnet build # TODO: Testing OPAQUE building, remove this when done and add it to pack-and-release.yml? From c5c5268cc610d556dfc60b4a82916df0a4d0fae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 11:46:37 +0100 Subject: [PATCH 25/89] Collect all bindings in one artifact --- .../workflows/build-rust-cross-platform.yml | 65 ++++++++++++++++++- .github/workflows/build.yml | 2 +- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 33df5681..5c60d634 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -71,4 +71,67 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding* + path: | + rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll + rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so + rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.dylib + + collect_artifacts: + name: Collect and Upload All Artifacts + runs-on: ubuntu-22.04 + needs: build_rust + steps: + - name: Download x86_64-apple-darwin files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-x86_64-apple-darwin + path: runtimes/osx-x64/native + + - name: Download aarch64-apple-darwin files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-aarch64-apple-darwin + path: runtimes/osx-arm64/native + + - name: Download x86_64-pc-windows-msvc files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-x86_64-pc-windows-msvc + path: runtimes/win-x64/native + + - name: Download x86_64-unknown-linux-gnu files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-x86_64-unknown-linux-gnu + path: runtimes/linux-x64/native + + - name: Download aarch64-unknown-linux-gnu files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu + path: runtimes/linux-arm64/native + + - name: Download armv7-unknown-linux-gnueabihf files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf + path: runtimes/linux-arm/native + + - name: Download arm-unknown-linux-gnueabihf files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf + path: runtimes/linux-armv6/native + + - name: Download armv5te-unknown-linux-gnueabi files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-armv5te-unknown-linux-gnueabi + path: runtimes/linux-armel/native + + - name: Upload Combined Artifact + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: libopaque_ke_binding_all_files + path: | + runtimes/**/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d6b5b08..00536a5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: jobs: build_rust: - name: Build Rust + name: Build Rust Cross Platform uses: ./.github/workflows/build-rust-cross-platform.yml build-artifacts: From fd8fa35490ec974b7b88e58f63ae9d809f6e9e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 11:53:21 +0100 Subject: [PATCH 26/89] Fix upload --- .github/workflows/build-rust-cross-platform.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 5c60d634..2cb45cb8 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -71,10 +71,7 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: | - rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll - rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so - rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.dylib + path: rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} collect_artifacts: name: Collect and Upload All Artifacts From 646739bf98d9ffc404e3b04c9682afe164b19a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 11:58:21 +0100 Subject: [PATCH 27/89] Use full path --- .github/workflows/build-rust-cross-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 2cb45cb8..cd6b34f1 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -71,7 +71,7 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} + path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} collect_artifacts: name: Collect and Upload All Artifacts From 3c9a39b745216d2cee94cb4dd82131d1df2fc5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:09:11 +0100 Subject: [PATCH 28/89] upload doesn't support globs --- .github/workflows/build-rust-cross-platform.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index cd6b34f1..29c23357 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -67,11 +67,16 @@ jobs: run: cross build --target ${{ matrix.settings.target }} --release working-directory: extensions/Bitwarden.OPAQUE/rust + - name: Copy output library + run: | + mkdir libopaque_output + cp target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} libopaque_output/ + - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} + path: libopaque_output/* collect_artifacts: name: Collect and Upload All Artifacts From 7a80f0528a1e32b41601c7976736ce85368cb72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:11:27 +0100 Subject: [PATCH 29/89] Debug --- .github/workflows/build-rust-cross-platform.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 29c23357..541b136c 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -70,6 +70,8 @@ jobs: - name: Copy output library run: | mkdir libopaque_output + ls -la target/${{ matrix.settings.target }}/release + ls -la target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} cp target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} libopaque_output/ - name: Upload Artifact From 650b7e06542ef533281c4d19bae6709e03ab46c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:14:45 +0100 Subject: [PATCH 30/89] Path --- .github/workflows/build-rust-cross-platform.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 541b136c..bb4c6754 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -70,9 +70,9 @@ jobs: - name: Copy output library run: | mkdir libopaque_output - ls -la target/${{ matrix.settings.target }}/release - ls -la target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} - cp target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} libopaque_output/ + ls -la extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release + ls -la extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} + cp extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} libopaque_output/ - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 From b9bdf2fa7b1c99445a3400cca75903ab849a9676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:16:50 +0100 Subject: [PATCH 31/89] Remove debug logs --- .github/workflows/build-rust-cross-platform.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index bb4c6754..d5d66e1d 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -70,8 +70,6 @@ jobs: - name: Copy output library run: | mkdir libopaque_output - ls -la extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release - ls -la extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} cp extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} libopaque_output/ - name: Upload Artifact From 9381bd7c70a346576cc41dd4abd2f3f097263601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:20:38 +0100 Subject: [PATCH 32/89] Remove copy --- .github/workflows/build-rust-cross-platform.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index d5d66e1d..7b8d49a5 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -67,16 +67,11 @@ jobs: run: cross build --target ${{ matrix.settings.target }} --release working-directory: extensions/Bitwarden.OPAQUE/rust - - name: Copy output library - run: | - mkdir libopaque_output - cp extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.{dll,so,dylib} libopaque_output/ - - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: libopaque_output/* + path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.* collect_artifacts: name: Collect and Upload All Artifacts From b5908d22c5f4eebac015dccff706701ed9c4bdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:26:56 +0100 Subject: [PATCH 33/89] Try multiple paths --- .github/workflows/build-rust-cross-platform.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 7b8d49a5..1aaaf72b 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -71,7 +71,10 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.target }} - path: extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/*opaque_ke_binding.* + path: | + extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll + extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so + extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.dylib collect_artifacts: name: Collect and Upload All Artifacts From 0fffd25e054b87ddbeda894a1f8d1d42476f0f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:39:43 +0100 Subject: [PATCH 34/89] Simplify library copies --- .github/workflows/build.yml | 48 ++----------------- .../src/Bitwarden.OPAQUE.csproj | 42 +--------------- 2 files changed, 5 insertions(+), 85 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00536a5f..014886d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,53 +35,11 @@ jobs: # TODO: Testing OPAQUE building, remove this when done and add it to pack-and-release.yml? - - name: Download x86_64-apple-darwin files + - name: Download cross compiled library files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-x86_64-apple-darwin - path: extensions/Bitwarden.OPAQUE/rust/dist/osx-x64 - - - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-aarch64-apple-darwin - path: extensions/Bitwarden.OPAQUE/rust/dist/osx-arm64 - - - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-x86_64-pc-windows-msvc - path: extensions/Bitwarden.OPAQUE/rust/dist/win-x64 - - - name: Download x86_64-unknown-linux-gnu files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-x86_64-unknown-linux-gnu - path: extensions/Bitwarden.OPAQUE/rust/dist/linux-x64 - - - name: Download aarch64-unknown-linux-gnu files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu - path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm64 - - - name: Download armv7-unknown-linux-gnueabihf files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf - path: extensions/Bitwarden.OPAQUE/rust/dist/linux-arm - - - name: Download arm-unknown-linux-gnueabihf files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf - path: extensions/Bitwarden.OPAQUE/rust/dist/linux-armv6 - - - name: Download armv5te-unknown-linux-gnueabi files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-armv5te-unknown-linux-gnueabi - path: extensions/Bitwarden.OPAQUE/rust/dist/linux-armel + name: libopaque_ke_binding_all_files + path: extensions/Bitwarden.OPAQUE/rust/dist/ - name: Pack solution run: dotnet pack -c Release --output ./nuget-output diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index f065fa89..e6c6e21b 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -73,48 +73,10 @@ - + Always true - runtimes/osx-x64/native + runtimes/ - - - Always - true - runtimes/osx-arm64/native - - - - Always - true - runtimes/linux-x64/native - - - - Always - true - runtimes/linux-arm64/native - - - - Always - true - runtimes/linux-arm/native - - - - Always - true - runtimes/win-x64/native - - - \ No newline at end of file From 8205bc20d134bbef88faceede34ca1c435cd73f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:49:44 +0100 Subject: [PATCH 35/89] Use matrix to download artifacts --- .../workflows/build-rust-cross-platform.yml | 67 ++++++------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 1aaaf72b..166a8cf9 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -81,53 +81,30 @@ jobs: runs-on: ubuntu-22.04 needs: build_rust steps: - - name: Download x86_64-apple-darwin files + - name: Download artifacts uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-x86_64-apple-darwin - path: runtimes/osx-x64/native - - - name: Download aarch64-apple-darwin files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-aarch64-apple-darwin - path: runtimes/osx-arm64/native - - - name: Download x86_64-pc-windows-msvc files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-x86_64-pc-windows-msvc - path: runtimes/win-x64/native - - - name: Download x86_64-unknown-linux-gnu files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-x86_64-unknown-linux-gnu - path: runtimes/linux-x64/native - - - name: Download aarch64-unknown-linux-gnu files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu - path: runtimes/linux-arm64/native - - - name: Download armv7-unknown-linux-gnueabihf files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf - path: runtimes/linux-arm/native - - - name: Download arm-unknown-linux-gnueabihf files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf - path: runtimes/linux-armv6/native - - - name: Download armv5te-unknown-linux-gnueabi files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-armv5te-unknown-linux-gnueabi - path: runtimes/linux-armel/native + name: libopaque_ke_binding_files-${{ matrix.target }} + path: ${{ matrix.path }} + strategy: + matrix: + include: + - target: x86_64-apple-darwin + path: runtimes/osx-x64/native + - target: aarch64-apple-darwin + path: runtimes/osx-arm64/native + - target: x86_64-pc-windows-msvc + path: runtimes/win-x64/native + - target: x86_64-unknown-linux-gnu + path: runtimes/linux-x64/native + - target: aarch64-unknown-linux-gnu + path: runtimes/linux-arm64/native + - target: armv7-unknown-linux-gnueabihf + path: runtimes/linux-arm/native + - target: arm-unknown-linux-gnueabihf + path: runtimes/linux-armv6/native + - target: armv5te-unknown-linux-gnueabi + path: runtimes/linux-armel/native - name: Upload Combined Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 From 749829bccc70e8ab74a2e2fd990314b00d6e14e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 12:51:02 +0100 Subject: [PATCH 36/89] Revert "Use matrix to download artifacts" This reverts commit 8205bc20d134bbef88faceede34ca1c435cd73f7. --- .../workflows/build-rust-cross-platform.yml | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 166a8cf9..1aaaf72b 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -81,30 +81,53 @@ jobs: runs-on: ubuntu-22.04 needs: build_rust steps: - - name: Download artifacts + - name: Download x86_64-apple-darwin files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-${{ matrix.target }} - path: ${{ matrix.path }} - strategy: - matrix: - include: - - target: x86_64-apple-darwin - path: runtimes/osx-x64/native - - target: aarch64-apple-darwin - path: runtimes/osx-arm64/native - - target: x86_64-pc-windows-msvc - path: runtimes/win-x64/native - - target: x86_64-unknown-linux-gnu - path: runtimes/linux-x64/native - - target: aarch64-unknown-linux-gnu - path: runtimes/linux-arm64/native - - target: armv7-unknown-linux-gnueabihf - path: runtimes/linux-arm/native - - target: arm-unknown-linux-gnueabihf - path: runtimes/linux-armv6/native - - target: armv5te-unknown-linux-gnueabi - path: runtimes/linux-armel/native + name: libopaque_ke_binding_files-x86_64-apple-darwin + path: runtimes/osx-x64/native + + - name: Download aarch64-apple-darwin files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-aarch64-apple-darwin + path: runtimes/osx-arm64/native + + - name: Download x86_64-pc-windows-msvc files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-x86_64-pc-windows-msvc + path: runtimes/win-x64/native + + - name: Download x86_64-unknown-linux-gnu files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-x86_64-unknown-linux-gnu + path: runtimes/linux-x64/native + + - name: Download aarch64-unknown-linux-gnu files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu + path: runtimes/linux-arm64/native + + - name: Download armv7-unknown-linux-gnueabihf files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf + path: runtimes/linux-arm/native + + - name: Download arm-unknown-linux-gnueabihf files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf + path: runtimes/linux-armv6/native + + - name: Download armv5te-unknown-linux-gnueabi files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_files-armv5te-unknown-linux-gnueabi + path: runtimes/linux-armel/native - name: Upload Combined Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 From 88a6f39af80cb20041250afce0ab6b8f77c8f3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 13:00:50 +0100 Subject: [PATCH 37/89] Use dotnet RIDs for artifacts --- .../workflows/build-rust-cross-platform.yml | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 1aaaf72b..8a5a7250 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -13,23 +13,31 @@ jobs: settings: - os: macos-13 target: x86_64-apple-darwin + dotnet_rid: osx-x64 - os: macos-13 target: aarch64-apple-darwin + dotnet_rid: osx-arm64 - os: windows-2022 target: x86_64-pc-windows-msvc + dotnet_rid: win-x64 # caution: updating the linux runner OS version for GNU # targets will likely break the library for older OS versions. # prefer using oldest supported runner for for these targets - os: ubuntu-22.04 target: x86_64-unknown-linux-gnu + dotnet_rid: linux-x64 - os: ubuntu-22.04 target: aarch64-unknown-linux-gnu + dotnet_rid: linux-arm64 - os: ubuntu-22.04 target: armv7-unknown-linux-gnueabihf + dotnet_rid: linux-arm - os: ubuntu-22.04 target: arm-unknown-linux-gnueabihf + dotnet_rid: linux-armv6 - os: ubuntu-22.04 target: armv5te-unknown-linux-gnueabi + dotnet_rid: linux-armel steps: - name: Checkout @@ -70,7 +78,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: - name: libopaque_ke_binding_files-${{ matrix.settings.target }} + name: libopaque_ke_binding_files-${{ matrix.settings.dotnet_rid }} path: | extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so @@ -81,52 +89,52 @@ jobs: runs-on: ubuntu-22.04 needs: build_rust steps: - - name: Download x86_64-apple-darwin files + - name: Download osx-x64 files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-x86_64-apple-darwin + name: libopaque_ke_binding_files-osx-x64 path: runtimes/osx-x64/native - - name: Download aarch64-apple-darwin files + - name: Download osx-arm64 files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-aarch64-apple-darwin + name: libopaque_ke_binding_files-osx-arm64 path: runtimes/osx-arm64/native - - name: Download x86_64-pc-windows-msvc files + - name: Download win-x64 files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-x86_64-pc-windows-msvc + name: libopaque_ke_binding_files-win-x64 path: runtimes/win-x64/native - - name: Download x86_64-unknown-linux-gnu files + - name: Download linux-x64 files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-x86_64-unknown-linux-gnu + name: libopaque_ke_binding_files-linux-x64 path: runtimes/linux-x64/native - - name: Download aarch64-unknown-linux-gnu files + - name: Download linux-arm64 files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-aarch64-unknown-linux-gnu + name: libopaque_ke_binding_files-linux-arm64 path: runtimes/linux-arm64/native - - name: Download armv7-unknown-linux-gnueabihf files + - name: Download linux-arm files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-armv7-unknown-linux-gnueabihf + name: libopaque_ke_binding_files-linux-arm path: runtimes/linux-arm/native - - name: Download arm-unknown-linux-gnueabihf files + - name: Download linux-armv6 files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-arm-unknown-linux-gnueabihf + name: libopaque_ke_binding_files-linux-armv6 path: runtimes/linux-armv6/native - - name: Download armv5te-unknown-linux-gnueabi files + - name: Download linux-armel files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-armv5te-unknown-linux-gnueabi + name: libopaque_ke_binding_files-linux-armel path: runtimes/linux-armel/native - name: Upload Combined Artifact From 744c3d932b147bf2ad9e149aa701a62587c04aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 13:07:44 +0100 Subject: [PATCH 38/89] Download all artifacts in one go --- .../workflows/build-rust-cross-platform.yml | 48 ++----------------- 1 file changed, 3 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 8a5a7250..94f4872d 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -89,53 +89,11 @@ jobs: runs-on: ubuntu-22.04 needs: build_rust steps: - - name: Download osx-x64 files + - name: Download all artifacts uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: - name: libopaque_ke_binding_files-osx-x64 - path: runtimes/osx-x64/native - - - name: Download osx-arm64 files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-osx-arm64 - path: runtimes/osx-arm64/native - - - name: Download win-x64 files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-win-x64 - path: runtimes/win-x64/native - - - name: Download linux-x64 files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-linux-x64 - path: runtimes/linux-x64/native - - - name: Download linux-arm64 files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-linux-arm64 - path: runtimes/linux-arm64/native - - - name: Download linux-arm files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-linux-arm - path: runtimes/linux-arm/native - - - name: Download linux-armv6 files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-linux-armv6 - path: runtimes/linux-armv6/native - - - name: Download linux-armel files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_files-linux-armel - path: runtimes/linux-armel/native + pattern: libopaque_ke_binding_files-* + path: runtimes/ - name: Upload Combined Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 From a241a22e653e08828406ba5303006da67f1e575d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 13:33:36 +0100 Subject: [PATCH 39/89] Move files to the correct path --- .github/workflows/build-rust-cross-platform.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 94f4872d..ea04dc80 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -93,7 +93,17 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: libopaque_ke_binding_files-* - path: runtimes/ + path: downloaded_runtimes/ + + - name: Move files to the correct directory + run: | + for file in downloaded_runtimes/libopaque_ke_binding_files-*; do + echo "Processing $file" + platform="${file#downloaded_runtimes/libopaque_ke_binding_files-}" + echo "Platform: $platform" + mkdir -p runtimes/${platform}/native + mv $file/* runtimes/${platform}/native + done - name: Upload Combined Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 From e4a716a6e4ecd1342e52eb8b80eefbfd12b81ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 14:16:35 +0100 Subject: [PATCH 40/89] Update workflows --- .github/workflows/build-rust-cross-platform.yml | 4 ++++ .github/workflows/build.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index ea04dc80..1d1d1410 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -79,6 +79,9 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_files-${{ matrix.settings.dotnet_rid }} + # We only need these until the next step, so delete them as soon as possible + retention-days: 1 + if-no-files-found: error path: | extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so @@ -109,5 +112,6 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: libopaque_ke_binding_all_files + if-no-files-found: error path: | runtimes/**/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 014886d8..e420aaef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,5 +48,6 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden.OPAQUE.nupkg + if-no-files-found: error path: | ./nuget-output/Bitwarden.OPAQUE*.nupkg From 194dd03742d10fd751e01c7da773be627c44aead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 14:49:30 +0100 Subject: [PATCH 41/89] Make rust-analyzer load correctly --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7d5485af --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.linkedProjects": ["extensions/Bitwarden.OPAQUE/rust/Cargo.toml"], +} From c82b0c648c099bdee765158e354917b1c3aa9ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 15:36:00 +0100 Subject: [PATCH 42/89] Docs & better code organization --- .../Bitwarden.OPAQUE/rust/src/client.rs | 12 ++-- extensions/Bitwarden.OPAQUE/rust/src/ffi.rs | 36 ++++++----- extensions/Bitwarden.OPAQUE/rust/src/lib.rs | 6 +- .../Bitwarden.OPAQUE/rust/src/server.rs | 42 ++++++++++--- .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 6 +- .../Bitwarden.OPAQUE/src/BitwardenOpaque.cs | 62 ------------------ .../src/BitwardenOpaqueClient.cs | 63 +++++++++++++++++++ .../src/BitwardenOpaqueServer.cs | 57 +++++++++++++++++ .../src/CipherConfiguration.cs | 55 ++++++++++++++++ extensions/Bitwarden.OPAQUE/tests/Tests.cs | 17 ++--- 10 files changed, 248 insertions(+), 108 deletions(-) delete mode 100644 extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs create mode 100644 extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs create mode 100644 extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs create mode 100644 extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs diff --git a/extensions/Bitwarden.OPAQUE/rust/src/client.rs b/extensions/Bitwarden.OPAQUE/rust/src/client.rs index 2bcc320f..a33c2464 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/client.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/client.rs @@ -10,7 +10,7 @@ use crate::*; pub(crate) struct ClientRegistrationStartResult { // The message is sent to the server for the next step of the registration protocol. - pub(crate) message: Vec, + pub(crate) registration_request: Vec, // The state is stored temporarily by the client and used in the next step of the registration protocol. pub(crate) state: Vec, } @@ -25,21 +25,21 @@ where let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; Ok(ClientRegistrationStartResult { - message: result.message.serialize().to_vec(), + registration_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) } pub(crate) struct ClientRegistrationFinishResult { // The message is sent to the server for the last step of the registration protocol. - pub(crate) message: Vec, + pub(crate) registration_upload: Vec, pub(crate) export_key: Vec, pub(crate) server_s_pk: Vec, } pub(crate) fn finish_client_registration( state: &[u8], - registration_response_bytes: &[u8], + registration_response: &[u8], password: &str, ) -> Result where @@ -55,12 +55,12 @@ where let result = state.finish( &mut OsRng, password.as_bytes(), - RegistrationResponse::deserialize(registration_response_bytes)?, + RegistrationResponse::deserialize(registration_response)?, ClientRegistrationFinishParameters::default(), )?; Ok(ClientRegistrationFinishResult { - message: result.message.serialize().to_vec(), + registration_upload: result.message.serialize().to_vec(), export_key: result.export_key.to_vec(), server_s_pk: result.server_s_pk.serialize().to_vec(), }) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs index ab1c553d..0d16f900 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs @@ -19,10 +19,10 @@ unsafe fn handle_string_input<'a>( /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_registration( - request_bytes: Buffer, + registration_request: Buffer, username: *const c_char, ) -> Response { - let registration_request_bytes = unsafe { request_bytes.as_slice() }; + let registration_request = unsafe { registration_request.as_slice() }; let username = match unsafe { handle_string_input(username, "username") } { Ok(s) => s, Err(e) => { @@ -31,7 +31,7 @@ pub unsafe extern "C" fn start_server_registration( }; let response = match super::server::start_server_registration::( - registration_request_bytes, + registration_request, username, ) { Ok(response) => response, @@ -40,7 +40,7 @@ pub unsafe extern "C" fn start_server_registration( } }; - Response::ok2(response.message, response.server_setup) + Response::ok2(response.registration_response, response.server_setup) } /// @@ -48,11 +48,11 @@ pub unsafe extern "C" fn start_server_registration( /// # Safety /// ABC #[unsafe(no_mangle)] -pub unsafe extern "C" fn finish_server_registration(registration_upload_bytes: Buffer) -> Response { - let registration_upload_bytes = unsafe { registration_upload_bytes.as_slice() }; +pub unsafe extern "C" fn finish_server_registration(registration_upload: Buffer) -> Response { + let registration_upload = unsafe { registration_upload.as_slice() }; let response = match super::server::finish_server_registration::( - registration_upload_bytes, + registration_upload, ) { Ok(response) => response, Err(e) => { @@ -60,7 +60,7 @@ pub unsafe extern "C" fn finish_server_registration(registration_upload_bytes: B } }; - Response::ok1(response) + Response::ok1(response.server_registration) } /// @@ -83,7 +83,7 @@ pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> R } }; - Response::ok2(result.message, result.state) + Response::ok2(result.registration_request, result.state) } /// @@ -92,12 +92,12 @@ pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> R /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_registration( - state_bytes: Buffer, - registration_response_bytes: Buffer, + state: Buffer, + registration_response: Buffer, password: *const c_char, ) -> Response { - let registration_response_bytes = unsafe { registration_response_bytes.as_slice() }; - let state_bytes = unsafe { state_bytes.as_slice() }; + let registration_response = unsafe { registration_response.as_slice() }; + let state = unsafe { state.as_slice() }; let password = match unsafe { handle_string_input(password, "password") } { Ok(s) => s, @@ -107,8 +107,8 @@ pub unsafe extern "C" fn finish_client_registration( }; let response = match super::client::finish_client_registration::( - state_bytes, - registration_response_bytes, + state, + registration_response, password, ) { Ok(response) => response, @@ -117,5 +117,9 @@ pub unsafe extern "C" fn finish_client_registration( } }; - Response::ok3(response.message, response.export_key, response.server_s_pk) + Response::ok3( + response.registration_upload, + response.export_key, + response.server_s_pk, + ) } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs index 3667bd65..2b3c4638 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs @@ -65,20 +65,20 @@ mod tests { client::start_client_registration::(password).unwrap(); let server_start_result = server::start_server_registration::( - ®istration_request.message, + ®istration_request.registration_request, username, ) .unwrap(); let client_finish_result = client::finish_client_registration::( ®istration_request.state, - &server_start_result.message, + &server_start_result.registration_response, password, ) .unwrap(); let server_finish_result = - server::finish_server_registration::(&client_finish_result.message) + server::finish_server_registration::(&client_finish_result.registration_upload) .unwrap(); let _ = server_finish_result; diff --git a/extensions/Bitwarden.OPAQUE/rust/src/server.rs b/extensions/Bitwarden.OPAQUE/rust/src/server.rs index 769c6696..843d67c9 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/server.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/server.rs @@ -9,12 +9,12 @@ use voprf::Group; use crate::*; pub(crate) struct ServerRegistrationStartResult { - pub(crate) message: Vec, + pub(crate) registration_response: Vec, pub(crate) server_setup: Vec, } pub(crate) fn start_server_registration( - registration_request_bytes: &[u8], + registration_request: &[u8], username: &str, ) -> Result where @@ -31,19 +31,23 @@ where let result = ServerRegistration::start( &server_setup, - RegistrationRequest::deserialize(registration_request_bytes)?, + RegistrationRequest::deserialize(registration_request)?, username.as_bytes(), )?; Ok(ServerRegistrationStartResult { - message: result.message.serialize().to_vec(), + registration_response: result.message.serialize().to_vec(), server_setup: server_setup.serialize().to_vec(), }) } +pub(crate) struct ServerRegistrationFinishResult { + pub(crate) server_registration: Vec, +} + pub(crate) fn finish_server_registration( - registration_upload_bytes: &[u8], -) -> Result, Error> + registration_upload: &[u8], +) -> Result where NonceLen: Add>>, EnvelopeLen: ArrayLength, @@ -52,8 +56,26 @@ where ArrayLength + Add>, RegistrationUploadLen: ArrayLength, { - let registration = ServerRegistration::finish(RegistrationUpload::::deserialize( - registration_upload_bytes, - )?); - Ok(registration.serialize().to_vec()) + let registration = + ServerRegistration::finish(RegistrationUpload::::deserialize(registration_upload)?); + Ok(ServerRegistrationFinishResult { + server_registration: registration.serialize().to_vec(), + }) +} + +/* +pub(crate) struct ServerLoginStartResult {} + +pub(crate) fn start_server_login( + login_start: &[u8], + username: &str, +) -> Result +where + +{ + + + // let result = ServerLogin::start(LoginStart::deserialize(login_start)?, username.as_bytes())?; + (()) Ok(ServerLoginStartResult {}) } +*/ diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs index 02aad2fa..7e0464fb 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -38,16 +38,16 @@ private struct Response private static partial void free_buffer(Buffer buf); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_server_registration(Buffer request_bytes, string username); + private static partial Response start_server_registration(Buffer registration_request, string username); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_server_registration(Buffer registration_upload_bytes); + private static partial Response finish_server_registration(Buffer registration_upload); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial Response start_client_registration(string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_client_registration(Buffer state_bytes, Buffer registration_response_bytes, string password); + private static partial Response finish_client_registration(Buffer state, Buffer registration_response, string password); private static Buffer BuildBuffer(byte[] data, out GCHandle handle) diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs deleted file mode 100644 index 37ac2381..00000000 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaque.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Bitwarden.OPAQUE; - -public enum OprfCS -{ - Ristretto255 -} - -public enum KeGroup -{ - Ristretto255 -} - -public enum KeyExchange -{ - TripleDH -} - -public abstract record KSF; - -public record Argon2id(int iterations, int memoryKiB, int parallelism) : KSF; - -public struct CipherConfiguration -{ - public OprfCS OprfCS; - public KeGroup KeGroup; - public KeyExchange KeyExchange; - public KSF KSF; - - public static readonly CipherConfiguration Default = new CipherConfiguration - { - OprfCS = OprfCS.Ristretto255, - KeGroup = KeGroup.Ristretto255, - KeyExchange = KeyExchange.TripleDH, - KSF = new Argon2id(4, 65536, 4) - }; - -} - -public sealed partial class BitwardenOpaque -{ - - - public (byte[], byte[]) StartServerRegistration(CipherConfiguration config, byte[] requestBytes, string username) - { - return BitwardenLibrary.StartServerRegistration(requestBytes, username); - } - - public byte[] FinishServerRegistration(CipherConfiguration config, byte[] registrationUploadBytes) - { - return BitwardenLibrary.FinishServerRegistration(registrationUploadBytes); - } - - public (byte[], byte[]) StartClientRegistration(CipherConfiguration config, string password) - { - return BitwardenLibrary.StartClientRegistration(password); - } - - public (byte[], byte[], byte[]) FinishClientRegistration(CipherConfiguration config, byte[] stateBytes, byte[] registrationResponseBytes, string password) - { - return BitwardenLibrary.FinishClientRegistration(stateBytes, registrationResponseBytes, password); - } -} diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs new file mode 100644 index 00000000..b78252a4 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs @@ -0,0 +1,63 @@ +namespace Bitwarden.OPAQUE; +#pragma warning disable CA1822 // Mark members as static + +/// The result of +public struct ClientRegistrationStartResult +{ + /// The registration response which is then passed to . + public byte[] registrationRequest; + + /// The client state, which must be kept on the client for . + public byte[] state; +} + +/// The result of +public struct ClientRegistrationFinishResult +{ + /// The registration upload which is then passed to . + public byte[] registrationUpload; + /// The export key output by client registration + public byte[] exportKey; + /// The server's static public key + public byte[] serverSPKey; +} + +/// A class to represent client side functionality the Bitwarden OPAQUE library. +public sealed partial class BitwardenOpaqueClient +{ + + /// + /// Start the client registration process. This is the first step in the registration process. + /// + /// The Cipher configuration, must be the same for all the operation + /// The password to register + /// + public ClientRegistrationStartResult StartRegistration(CipherConfiguration config, string password) + { + var (registrationRequest, state) = BitwardenLibrary.StartClientRegistration(password); + return new ClientRegistrationStartResult + { + registrationRequest = registrationRequest, + state = state + }; + } + + /// + /// Finish the server registration process. This must happen after + /// + /// The Cipher configuration, must be the same for all the operation + /// The state obtained from the client start operation, + /// The server registration response, + /// The password to register + /// + public ClientRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) + { + var (registrationUpload, exportKey, serverSPKey) = BitwardenLibrary.FinishClientRegistration(state, registrationResponse, password); + return new ClientRegistrationFinishResult + { + registrationUpload = registrationUpload, + exportKey = exportKey, + serverSPKey = serverSPKey + }; + } +} diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs new file mode 100644 index 00000000..8579316f --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs @@ -0,0 +1,57 @@ +namespace Bitwarden.OPAQUE; +#pragma warning disable CA1822 // Mark members as static + + +/// The result of +public struct ServerRegistrationStartResult +{ + /// The registration response which is then passed to . + public byte[] registrationResponse; + /// The server setup, which needs to be persisted on the server for future logins. + public byte[] serverSetup; +} + +/// The result of +public struct ServerRegistrationFinishResult + +{ + /// The server registration, which needs to be persisted on the server for future logins. + public byte[] serverRegistration; +} + +/// A class to represent server side functionality the Bitwarden OPAQUE library. +public sealed partial class BitwardenOpaqueServer +{ + /// + /// Start the server registration process. This must happen after + /// + /// The Cipher configuration, must be the same for all the operation + /// The client registration request, + /// The username to register + /// + public ServerRegistrationStartResult StartRegistration(CipherConfiguration config, byte[] registrationRequest, string username) + { + var (registrationResponse, serverSetup) = BitwardenLibrary.StartServerRegistration(registrationRequest, username); + return new ServerRegistrationStartResult + { + registrationResponse = registrationResponse, + serverSetup = serverSetup + }; + } + + /// + /// Finish the server registration process. This must happen after + /// + /// The Cipher configuration, must be the same for all the operation + /// The client registration upload, + /// + public ServerRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] registrationUpload) + { + var serverRegistration = BitwardenLibrary.FinishServerRegistration(registrationUpload); + return new ServerRegistrationFinishResult + { + serverRegistration = serverRegistration + }; + } + +} diff --git a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs new file mode 100644 index 00000000..ccf44878 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs @@ -0,0 +1,55 @@ +namespace Bitwarden.OPAQUE; + +/// A VOPRF ciphersuite +public enum OprfCS +{ + /// The Ristretto255 ciphersuite + Ristretto255 = 0, +} + +/// A `Group` used for the `KeyExchange`. +public enum KeGroup +{ + /// The Ristretto255 group + Ristretto255 +} + +/// The key exchange protocol to use in the login step +public enum KeyExchange +{ + /// The Triple Diffie-Hellman key exchange implementation + TripleDH +} + +/// A key stretching function, typically used for password hashing +public abstract record KSF; + +/// +/// Argon2id key stretching function +/// +/// Iteration count +/// Memory in KibiBytes +/// Parallelism count +public record Argon2id(int iterations, int memoryKiB, int parallelism) : KSF; + +/// Configures the underlying primitives used in OPAQUE +public struct CipherConfiguration +{ + /// A VOPRF ciphersuite + public OprfCS OprfCS; + /// A `Group` used for the `KeyExchange`. + public KeGroup KeGroup; + /// The key exchange protocol to use in the login step + public KeyExchange KeyExchange; + /// A key stretching function, typically used for password hashing + public KSF KSF; + + /// The default configuration for the OPAQUE protocol + public static readonly CipherConfiguration Default = new CipherConfiguration + { + OprfCS = OprfCS.Ristretto255, + KeGroup = KeGroup.Ristretto255, + KeyExchange = KeyExchange.TripleDH, + KSF = new Argon2id(4, 65536, 4) + }; +} diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index 189618ff..07438591 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -5,29 +5,30 @@ namespace Bitwarden.OPAQUE.Tests; public class SampleTests { [Fact] - public void RunSample_Works() + public void TestRegistration() { // Get environment variables var username = "demo_username"; var password = "demo_password"; - // Create the OPAQUE Client - var client = new BitwardenOpaque(); + // Create the OPAQUE Clients + var server = new BitwardenOpaqueServer(); + var client = new BitwardenOpaqueClient(); var config = CipherConfiguration.Default; // Start the client registration - var (clientRequest, clientState) = client.StartClientRegistration(config, password); + var clientStartResult = client.StartRegistration(config, password); // Client sends reg_start to server - var (serverResponse, serverSetup) = client.StartServerRegistration(config, clientRequest, username); + var serverStartResult = server.StartRegistration(config, clientStartResult.registrationRequest, username); // Server sends server_start_result to client - var (registrationUpload, exportKey, serverSPKey) = client.FinishClientRegistration(config, clientState, serverResponse, password); + var clientFinishResult = client.FinishRegistration(config, clientStartResult.state, serverStartResult.registrationResponse, password); // Client sends client_finish_result to server - var result = client.FinishServerRegistration(config, registrationUpload); + var serverFinishResult = server.FinishRegistration(config, clientFinishResult.registrationUpload); - Assert.NotNull(result); + Assert.NotNull(serverFinishResult.serverRegistration); } } From a7aef23f0c55b09165239355228e245b9723c790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 17:44:49 +0100 Subject: [PATCH 43/89] Implement dynamic dispatch for opaque crate, add server setup param --- .../Bitwarden.OPAQUE/rust/src/client.rs | 67 ------ .../rust/src/{ffi.rs => ffi/mod.rs} | 51 +++-- .../rust/src/{ffi_types.rs => ffi/types.rs} | 7 + extensions/Bitwarden.OPAQUE/rust/src/lib.rs | 68 ++---- .../Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 204 ++++++++++++++++++ .../Bitwarden.OPAQUE/rust/src/opaque/types.rs | 72 +++++++ .../Bitwarden.OPAQUE/rust/src/server.rs | 81 ------- .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 30 +-- .../src/BitwardenOpaqueServer.cs | 7 +- extensions/Bitwarden.OPAQUE/tests/Tests.cs | 2 +- 10 files changed, 354 insertions(+), 235 deletions(-) delete mode 100644 extensions/Bitwarden.OPAQUE/rust/src/client.rs rename extensions/Bitwarden.OPAQUE/rust/src/{ffi.rs => ffi/mod.rs} (69%) rename extensions/Bitwarden.OPAQUE/rust/src/{ffi_types.rs => ffi/types.rs} (92%) create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs create mode 100644 extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs delete mode 100644 extensions/Bitwarden.OPAQUE/rust/src/server.rs diff --git a/extensions/Bitwarden.OPAQUE/rust/src/client.rs b/extensions/Bitwarden.OPAQUE/rust/src/client.rs deleted file mode 100644 index a33c2464..00000000 --- a/extensions/Bitwarden.OPAQUE/rust/src/client.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::ops::Add; - -use digest::typenum::Sum; -use generic_array::ArrayLength; -use opaque_ke::{key_exchange::group::KeGroup, *}; -use rand::rngs::OsRng; -use voprf::Group; - -use crate::*; - -pub(crate) struct ClientRegistrationStartResult { - // The message is sent to the server for the next step of the registration protocol. - pub(crate) registration_request: Vec, - // The state is stored temporarily by the client and used in the next step of the registration protocol. - pub(crate) state: Vec, -} - -pub(crate) fn start_client_registration( - password: &str, -) -> Result -where - as Group>::ScalarLen: Add< as Group>::ElemLen>, - ClientRegistrationLen: ArrayLength, -{ - let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; - - Ok(ClientRegistrationStartResult { - registration_request: result.message.serialize().to_vec(), - state: result.state.serialize().to_vec(), - }) -} - -pub(crate) struct ClientRegistrationFinishResult { - // The message is sent to the server for the last step of the registration protocol. - pub(crate) registration_upload: Vec, - pub(crate) export_key: Vec, - pub(crate) server_s_pk: Vec, -} - -pub(crate) fn finish_client_registration( - state: &[u8], - registration_response: &[u8], - password: &str, -) -> Result -where - NonceLen: Add>>, - EnvelopeLen: ArrayLength, - ::PkLen: Add>>, - Sum<::PkLen, OutputSize>>: - ArrayLength + Add>, - RegistrationUploadLen: ArrayLength, -{ - let state = ClientRegistration::::deserialize(state)?; - - let result = state.finish( - &mut OsRng, - password.as_bytes(), - RegistrationResponse::deserialize(registration_response)?, - ClientRegistrationFinishParameters::default(), - )?; - - Ok(ClientRegistrationFinishResult { - registration_upload: result.message.serialize().to_vec(), - export_key: result.export_key.to_vec(), - server_s_pk: result.server_s_pk.serialize().to_vec(), - }) -} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs similarity index 69% rename from extensions/Bitwarden.OPAQUE/rust/src/ffi.rs rename to extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs index 0d16f900..d3fe273b 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs @@ -1,6 +1,13 @@ use std::ffi::c_char; -use crate::{DefaultCipherSuite, Error, ffi_types::*}; +use crate::{ + Error, + opaque::{CipherConfiguration, OpaqueImpl}, +}; + +mod types; + +use types::*; unsafe fn handle_string_input<'a>( input: *const c_char, @@ -19,9 +26,11 @@ unsafe fn handle_string_input<'a>( /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_registration( + server_setup: Buffer, registration_request: Buffer, username: *const c_char, ) -> Response { + let server_setup = unsafe { server_setup.as_slice2() }.ok(); let registration_request = unsafe { registration_request.as_slice() }; let username = match unsafe { handle_string_input(username, "username") } { Ok(s) => s, @@ -30,15 +39,16 @@ pub unsafe extern "C" fn start_server_registration( } }; - let response = match super::server::start_server_registration::( - registration_request, - username, - ) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let response = + match config.start_server_registration(server_setup, registration_request, username) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; Response::ok2(response.registration_response, response.server_setup) } @@ -51,9 +61,10 @@ pub unsafe extern "C" fn start_server_registration( pub unsafe extern "C" fn finish_server_registration(registration_upload: Buffer) -> Response { let registration_upload = unsafe { registration_upload.as_slice() }; - let response = match super::server::finish_server_registration::( - registration_upload, - ) { + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let response = match config.finish_server_registration(registration_upload) { Ok(response) => response, Err(e) => { return Response::error(e); @@ -76,7 +87,10 @@ pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> R } }; - let result = match super::client::start_client_registration::(password) { + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let result = match config.start_client_registration(password) { Ok(result) => result, Err(e) => { return Response::error(e); @@ -106,11 +120,10 @@ pub unsafe extern "C" fn finish_client_registration( } }; - let response = match super::client::finish_client_registration::( - state, - registration_response, - password, - ) { + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let response = match config.finish_client_registration(state, registration_response, password) { Ok(response) => response, Err(e) => { return Response::error(e); diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs similarity index 92% rename from extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs rename to extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs index 2975dab6..3ccb5351 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi_types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs @@ -40,6 +40,13 @@ impl Buffer { unsafe { std::slice::from_raw_parts(self.data, self.len) } } + pub unsafe fn as_slice2(&self) -> Result<&[u8], Error> { + if self.data.is_null() { + return Err(Error::InvalidInput("Buffer data is null")); + } + Ok(unsafe { std::slice::from_raw_parts(self.data, self.len) }) + } + pub fn free(self) { if !self.data.is_null() { let _ = unsafe { Vec::from_raw_parts(self.data, self.len, self.len) }; diff --git a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs index 2b3c4638..a975e236 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs @@ -5,10 +5,8 @@ use zeroizing_alloc::ZeroAlloc; #[global_allocator] static ALLOC: ZeroAlloc = ZeroAlloc(std::alloc::System); -mod client; mod ffi; -mod ffi_types; -mod server; +mod opaque; #[derive(Debug)] pub enum Error { @@ -22,64 +20,34 @@ impl From for Error { } } -// These type aliases were copied from the `opaque_ke` crate, as they are not public, but help with the type signatures. -pub(crate) type OutputSize = - <::Core as digest::OutputSizeUser>::OutputSize; -pub(crate) type OprfGroup = - <::OprfCs as voprf::CipherSuite>::Group; -pub(crate) type OprfHash = <::OprfCs as voprf::CipherSuite>::Hash; -type NonceLen = digest::consts::U32; -pub(crate) type EnvelopeLen = digest::typenum::Sum>>; -pub(crate) type ClientRegistrationLen = digest::typenum::Sum< - as voprf::Group>::ScalarLen, - as voprf::Group>::ElemLen, ->; -pub(crate) type ServerSetupLen = digest::typenum::Sum< - digest::typenum::Sum< - OutputSize>, - ::KeGroup>>::Len, - >, - <::KeGroup as opaque_ke::key_exchange::group::KeGroup>::SkLen, ->; - -pub struct DefaultCipherSuite; - -impl opaque_ke::CipherSuite for DefaultCipherSuite { - type OprfCs = opaque_ke::Ristretto255; - type KeGroup = opaque_ke::Ristretto255; - type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; - - type Ksf = argon2::Argon2<'static>; -} - #[cfg(test)] mod tests { - use super::*; + use crate::opaque::*; #[test] fn test() { let password = "password"; let username = "username"; - let registration_request = - client::start_client_registration::(password).unwrap(); + let config = CipherConfiguration::default(); + + let registration_request = config.start_client_registration(password).unwrap(); - let server_start_result = server::start_server_registration::( - ®istration_request.registration_request, - username, - ) - .unwrap(); + let server_start_result = config + .start_server_registration(None, ®istration_request.registration_request, username) + .unwrap(); - let client_finish_result = client::finish_client_registration::( - ®istration_request.state, - &server_start_result.registration_response, - password, - ) - .unwrap(); + let client_finish_result = config + .finish_client_registration( + ®istration_request.state, + &server_start_result.registration_response, + password, + ) + .unwrap(); - let server_finish_result = - server::finish_server_registration::(&client_finish_result.registration_upload) - .unwrap(); + let server_finish_result = config + .finish_server_registration(&client_finish_result.registration_upload) + .unwrap(); let _ = server_finish_result; } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs new file mode 100644 index 00000000..6bd3f3cf --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -0,0 +1,204 @@ +use argon2::Argon2; +use opaque_ke::*; +use rand::rngs::OsRng; + +use crate::Error; + +mod types; + +pub(crate) use types::*; + +// The opaque-ke crate uses a lot of generic traits, which are difficult to handle in FFI. +// This trait implements dynamic dispatch to allow using opaque-ke without generics. +pub trait OpaqueImpl { + fn start_client_registration( + &self, + password: &str, + ) -> Result; + fn finish_client_registration( + &self, + state: &[u8], + registration_response: &[u8], + password: &str, + ) -> Result; + fn start_server_registration( + &self, + server_setup: Option<&[u8]>, + registration_request: &[u8], + username: &str, + ) -> Result; + fn finish_server_registration( + &self, + registration_upload: &[u8], + ) -> Result; +} + +// Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. +#[allow(unreachable_patterns)] +impl OpaqueImpl for CipherConfiguration { + fn start_client_registration( + &self, + password: &str, + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon).start_client_registration(password), + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + + fn finish_client_registration( + &self, + state: &[u8], + registration_response: &[u8], + password: &str, + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon).finish_client_registration( + state, + registration_response, + password, + ), + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + + fn start_server_registration( + &self, + server_setup: Option<&[u8]>, + registration_request: &[u8], + username: &str, + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon).start_server_registration( + server_setup, + registration_request, + username, + ), + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + + fn finish_server_registration( + &self, + registration_upload: &[u8], + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => { + RistrettoTripleDhArgonSuite(*argon).finish_server_registration(registration_upload) + } + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } +} + +// Define the cipher suite and implement OpaqueImpl for it. +// Note that in the future if we want to support multiple cipher suites, +// we will need to duplicate most of this code. It should be entirely the same, +// with the exception of the KDF settings, so we should build a macro for that +struct RistrettoTripleDhArgonSuite(Argon2id); +impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { + type OprfCs = opaque_ke::Ristretto255; + type KeGroup = opaque_ke::Ristretto255; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; + type Ksf = argon2::Argon2<'static>; +} + +impl OpaqueImpl for RistrettoTripleDhArgonSuite { + fn start_client_registration( + &self, + password: &str, + ) -> Result { + let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; + Ok(types::ClientRegistrationStartResult { + registration_request: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) + } + + fn finish_client_registration( + &self, + state: &[u8], + registration_response: &[u8], + password: &str, + ) -> Result { + let state = ClientRegistration::::deserialize(state)?; + let result = state.finish( + &mut OsRng, + password.as_bytes(), + RegistrationResponse::deserialize(registration_response)?, + ClientRegistrationFinishParameters::new( + Identifiers::default(), + Some(&Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new( + self.0.memory_kib, + self.0.iterations, + self.0.parallelism, + None, + ) + .map_err(|_| Error::InvalidInput("Invalid Argon2 parameters"))?, + )), + ), + )?; + + Ok(types::ClientRegistrationFinishResult { + registration_upload: result.message.serialize().to_vec(), + export_key: result.export_key.to_vec(), + server_s_pk: result.server_s_pk.serialize().to_vec(), + }) + } + + fn start_server_registration( + &self, + server_setup: Option<&[u8]>, + registration_request: &[u8], + username: &str, + ) -> Result { + let server_setup = match server_setup { + Some(server_setup) => ServerSetup::::deserialize(server_setup)?, + None => ServerSetup::::new(&mut OsRng), + }; + let result = ServerRegistration::start( + &server_setup, + RegistrationRequest::deserialize(registration_request)?, + username.as_bytes(), + )?; + Ok(types::ServerRegistrationStartResult { + registration_response: result.message.serialize().to_vec(), + server_setup: server_setup.serialize().to_vec(), + }) + } + + fn finish_server_registration( + &self, + registration_upload: &[u8], + ) -> Result { + let registration = ServerRegistration::finish(RegistrationUpload::::deserialize( + registration_upload, + )?); + Ok(types::ServerRegistrationFinishResult { + server_registration: registration.serialize().to_vec(), + }) + } +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs new file mode 100644 index 00000000..ee4092e1 --- /dev/null +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs @@ -0,0 +1,72 @@ +#[derive(Debug, Clone, Copy)] +pub enum OprfCs { + Ristretto255, +} + +#[derive(Debug, Clone, Copy)] +pub enum KeGroup { + Ristretto255, +} + +#[derive(Debug, Clone, Copy)] +pub enum KeyExchange { + TripleDh, +} + +#[derive(Debug, Clone, Copy)] +pub enum Ksf { + Argon2id(Argon2id), +} + +#[derive(Debug, Clone, Copy)] +pub struct Argon2id { + pub memory_kib: u32, + pub iterations: u32, + pub parallelism: u32, +} + +#[derive(Debug, Clone, Copy)] +pub struct CipherConfiguration { + pub oprf_cs: OprfCs, + pub ke_group: KeGroup, + pub key_exchange: KeyExchange, + pub ksf: Ksf, +} + +impl Default for CipherConfiguration { + fn default() -> Self { + Self { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(Argon2id { + memory_kib: 64, + iterations: 3, + parallelism: 1, + }), + } + } +} + +pub(crate) struct ClientRegistrationStartResult { + // The message is sent to the server for the next step of the registration protocol. + pub(crate) registration_request: Vec, + // The state is stored temporarily by the client and used in the next step of the registration protocol. + pub(crate) state: Vec, +} + +pub(crate) struct ClientRegistrationFinishResult { + // The message is sent to the server for the last step of the registration protocol. + pub(crate) registration_upload: Vec, + pub(crate) export_key: Vec, + pub(crate) server_s_pk: Vec, +} + +pub(crate) struct ServerRegistrationStartResult { + pub(crate) registration_response: Vec, + pub(crate) server_setup: Vec, +} + +pub(crate) struct ServerRegistrationFinishResult { + pub(crate) server_registration: Vec, +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/server.rs b/extensions/Bitwarden.OPAQUE/rust/src/server.rs deleted file mode 100644 index 843d67c9..00000000 --- a/extensions/Bitwarden.OPAQUE/rust/src/server.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::ops::Add; - -use digest::typenum::Sum; -use generic_array::ArrayLength; -use opaque_ke::{CipherSuite, key_exchange::group::KeGroup, keypair::PrivateKey, *}; -use rand::rngs::OsRng; -use voprf::Group; - -use crate::*; - -pub(crate) struct ServerRegistrationStartResult { - pub(crate) registration_response: Vec, - pub(crate) server_setup: Vec, -} - -pub(crate) fn start_server_registration( - registration_request: &[u8], - username: &str, -) -> Result -where - // Message::Serialize - as Group>::ElemLen: Add<::PkLen>, - RegistrationResponseLen: ArrayLength, - // ServerSetup::Serialize - OutputSize>: Add<<::KeGroup as KeGroup>::SkLen>, - Sum>, <::KeGroup as KeGroup>::SkLen>: - ArrayLength + Add<::SkLen>, - ServerSetupLen>: ArrayLength, -{ - let server_setup = ServerSetup::::new(&mut OsRng); - - let result = ServerRegistration::start( - &server_setup, - RegistrationRequest::deserialize(registration_request)?, - username.as_bytes(), - )?; - - Ok(ServerRegistrationStartResult { - registration_response: result.message.serialize().to_vec(), - server_setup: server_setup.serialize().to_vec(), - }) -} - -pub(crate) struct ServerRegistrationFinishResult { - pub(crate) server_registration: Vec, -} - -pub(crate) fn finish_server_registration( - registration_upload: &[u8], -) -> Result -where - NonceLen: Add>>, - EnvelopeLen: ArrayLength, - ::PkLen: Add>>, - Sum<::PkLen, OutputSize>>: - ArrayLength + Add>, - RegistrationUploadLen: ArrayLength, -{ - let registration = - ServerRegistration::finish(RegistrationUpload::::deserialize(registration_upload)?); - Ok(ServerRegistrationFinishResult { - server_registration: registration.serialize().to_vec(), - }) -} - -/* -pub(crate) struct ServerLoginStartResult {} - -pub(crate) fn start_server_login( - login_start: &[u8], - username: &str, -) -> Result -where - -{ - - - // let result = ServerLogin::start(LoginStart::deserialize(login_start)?, username.as_bytes())?; - (()) Ok(ServerLoginStartResult {}) -} -*/ diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs index 7e0464fb..e1d0fff7 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -38,7 +38,7 @@ private struct Response private static partial void free_buffer(Buffer buf); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_server_registration(Buffer registration_request, string username); + private static partial Response start_server_registration(Buffer server_setup, Buffer registration_request, string username); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial Response finish_server_registration(Buffer registration_upload); @@ -50,13 +50,13 @@ private struct Response private static partial Response finish_client_registration(Buffer state, Buffer registration_response, string password); - private static Buffer BuildBuffer(byte[] data, out GCHandle handle) + private static Buffer BuildBuffer(byte[]? data, out GCHandle handle) { handle = GCHandle.Alloc(data, GCHandleType.Pinned); return new Buffer { data = handle.AddrOfPinnedObject(), - size = data.Length + size = data?.Length ?? 0 }; } @@ -100,28 +100,30 @@ private static List HandleResponse(Response response, int expectedValues return arrays; } - internal static (byte[], byte[]) StartServerRegistration(byte[] requestBytes, string username) + internal static (byte[], byte[]) StartServerRegistration(byte[]? serverSetup, byte[] registrationRequest, string username) { - var requestBuffer = BuildBuffer(requestBytes, out var handle); + var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); + var registrationRequestBuf = BuildBuffer(registrationRequest, out var registrationRequestHandle); try { - var response = start_server_registration(requestBuffer, username); + var response = start_server_registration(serverSetupBuf, registrationRequestBuf, username); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); } finally { - handle.Free(); + serverSetupHandle.Free(); + registrationRequestHandle.Free(); } } - internal static byte[] FinishServerRegistration(byte[] registrationUploadBytes) + internal static byte[] FinishServerRegistration(byte[] registrationUpload) { - var registrationUploadBuffer = BuildBuffer(registrationUploadBytes, out var handle); + var registrationUploadBuf = BuildBuffer(registrationUpload, out var handle); try { - var response = finish_server_registration(registrationUploadBuffer); + var response = finish_server_registration(registrationUploadBuf); return HandleResponse(response, 1)[0]; } finally @@ -138,14 +140,14 @@ internal static (byte[], byte[]) StartClientRegistration(string password) return (ret[0], ret[1]); } - internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] stateBytes, byte[] registrationResponseBytes, string password) + internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] state, byte[] registrationResponse, string password) { - var stateBuffer = BuildBuffer(stateBytes, out var stateHandle); - var registrationResponseBuffer = BuildBuffer(registrationResponseBytes, out var registrationResponseHandle); + var stateBuf = BuildBuffer(state, out var stateHandle); + var registrationResponseBuf = BuildBuffer(registrationResponse, out var registrationResponseHandle); try { - var response = finish_client_registration(stateBuffer, registrationResponseBuffer, password); + var response = finish_client_registration(stateBuf, registrationResponseBuf, password); var ret = HandleResponse(response, 3); return (ret[0], ret[1], ret[2]); } diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs index 8579316f..a966cdec 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs @@ -26,16 +26,17 @@ public sealed partial class BitwardenOpaqueServer /// Start the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation + /// The server setup. Use null to let the library create a new random one /// The client registration request, /// The username to register /// - public ServerRegistrationStartResult StartRegistration(CipherConfiguration config, byte[] registrationRequest, string username) + public ServerRegistrationStartResult StartRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) { - var (registrationResponse, serverSetup) = BitwardenLibrary.StartServerRegistration(registrationRequest, username); + var (registrationResponse, serverSetupRet) = BitwardenLibrary.StartServerRegistration(serverSetup, registrationRequest, username); return new ServerRegistrationStartResult { registrationResponse = registrationResponse, - serverSetup = serverSetup + serverSetup = serverSetupRet }; } diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index 07438591..b723ee90 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -21,7 +21,7 @@ public void TestRegistration() var clientStartResult = client.StartRegistration(config, password); // Client sends reg_start to server - var serverStartResult = server.StartRegistration(config, clientStartResult.registrationRequest, username); + var serverStartResult = server.StartRegistration(config, null, clientStartResult.registrationRequest, username); // Server sends server_start_result to client var clientFinishResult = client.FinishRegistration(config, clientStartResult.state, serverStartResult.registrationResponse, password); From 64d9977b7f12f220aad6590ae090b4fa7763795b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 18:03:19 +0100 Subject: [PATCH 44/89] Implement login --- extensions/Bitwarden.OPAQUE/rust/src/lib.rs | 32 ++- .../Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 258 +++++++++++++++--- .../Bitwarden.OPAQUE/rust/src/opaque/types.rs | 23 ++ 3 files changed, 273 insertions(+), 40 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs index a975e236..60672a29 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs @@ -31,6 +31,8 @@ mod tests { let config = CipherConfiguration::default(); + // Registration + let registration_request = config.start_client_registration(password).unwrap(); let server_start_result = config @@ -49,6 +51,34 @@ mod tests { .finish_server_registration(&client_finish_result.registration_upload) .unwrap(); - let _ = server_finish_result; + // Login + + let login_request = config.start_client_login(password).unwrap(); + + let server_login_result = config + .start_server_login( + &server_start_result.server_setup, + &server_finish_result.server_registration, + &login_request.credential_request, + username, + ) + .unwrap(); + + let client_login_result = config + .finish_client_login( + &login_request.state, + &server_login_result.credential_response, + password, + ) + .unwrap(); + + let server_login_finish_result = config + .finish_server_login( + &server_login_result.state, + &client_login_result.credential_finalization, + ) + .unwrap(); + + let _ = server_login_finish_result.session_key; } } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs index 6bd3f3cf..f872ed75 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -15,22 +15,43 @@ pub trait OpaqueImpl { &self, password: &str, ) -> Result; - fn finish_client_registration( - &self, - state: &[u8], - registration_response: &[u8], - password: &str, - ) -> Result; fn start_server_registration( &self, server_setup: Option<&[u8]>, registration_request: &[u8], username: &str, ) -> Result; + fn finish_client_registration( + &self, + state: &[u8], + registration_response: &[u8], + password: &str, + ) -> Result; fn finish_server_registration( &self, registration_upload: &[u8], ) -> Result; + + fn start_client_login(&self, password: &str) -> Result; + fn start_server_login( + &self, + server_setup: &[u8], + server_registration: &[u8], + credential_request: &[u8], + username: &str, + ) -> Result; + fn finish_client_login( + &self, + state: &[u8], + credential_response: &[u8], + password: &str, + ) -> Result; + + fn finish_server_login( + &self, + state: &[u8], + credential_finalization: &[u8], + ) -> Result; } // Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. @@ -51,6 +72,27 @@ impl OpaqueImpl for CipherConfiguration { } } + fn start_server_registration( + &self, + server_setup: Option<&[u8]>, + registration_request: &[u8], + username: &str, + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon).start_server_registration( + server_setup, + registration_request, + username, + ), + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + fn finish_client_registration( &self, state: &[u8], @@ -72,40 +114,92 @@ impl OpaqueImpl for CipherConfiguration { } } - fn start_server_registration( + fn finish_server_registration( &self, - server_setup: Option<&[u8]>, - registration_request: &[u8], + registration_upload: &[u8], + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => { + RistrettoTripleDhArgonSuite(*argon).finish_server_registration(registration_upload) + } + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + + fn start_client_login(&self, password: &str) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon).start_client_login(password), + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + + fn start_server_login( + &self, + server_setup: &[u8], + server_registration: &[u8], + credential_request: &[u8], username: &str, - ) -> Result { + ) -> Result { match self { CipherConfiguration { oprf_cs: OprfCs::Ristretto255, ke_group: KeGroup::Ristretto255, key_exchange: KeyExchange::TripleDh, ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).start_server_registration( + } => RistrettoTripleDhArgonSuite(*argon).start_server_login( server_setup, - registration_request, + server_registration, + credential_request, username, ), _ => Err(Error::InvalidInput("Invalid cipher configuration")), } } - fn finish_server_registration( + fn finish_client_login( &self, - registration_upload: &[u8], - ) -> Result { + state: &[u8], + credential_response: &[u8], + password: &str, + ) -> Result { match self { CipherConfiguration { oprf_cs: OprfCs::Ristretto255, ke_group: KeGroup::Ristretto255, key_exchange: KeyExchange::TripleDh, ksf: Ksf::Argon2id(argon), - } => { - RistrettoTripleDhArgonSuite(*argon).finish_server_registration(registration_upload) - } + } => RistrettoTripleDhArgonSuite(*argon).finish_client_login( + state, + credential_response, + password, + ), + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + + fn finish_server_login( + &self, + state: &[u8], + credential_finalization: &[u8], + ) -> Result { + match self { + CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon) + .finish_server_login(state, credential_finalization), _ => Err(Error::InvalidInput("Invalid cipher configuration")), } } @@ -135,6 +229,27 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } + fn start_server_registration( + &self, + server_setup: Option<&[u8]>, + registration_request: &[u8], + username: &str, + ) -> Result { + let server_setup = match server_setup { + Some(server_setup) => ServerSetup::::deserialize(server_setup)?, + None => ServerSetup::::new(&mut OsRng), + }; + let result = ServerRegistration::start( + &server_setup, + RegistrationRequest::deserialize(registration_request)?, + username.as_bytes(), + )?; + Ok(types::ServerRegistrationStartResult { + registration_response: result.message.serialize().to_vec(), + server_setup: server_setup.serialize().to_vec(), + }) + } + fn finish_client_registration( &self, state: &[u8], @@ -169,27 +284,6 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } - fn start_server_registration( - &self, - server_setup: Option<&[u8]>, - registration_request: &[u8], - username: &str, - ) -> Result { - let server_setup = match server_setup { - Some(server_setup) => ServerSetup::::deserialize(server_setup)?, - None => ServerSetup::::new(&mut OsRng), - }; - let result = ServerRegistration::start( - &server_setup, - RegistrationRequest::deserialize(registration_request)?, - username.as_bytes(), - )?; - Ok(types::ServerRegistrationStartResult { - registration_response: result.message.serialize().to_vec(), - server_setup: server_setup.serialize().to_vec(), - }) - } - fn finish_server_registration( &self, registration_upload: &[u8], @@ -201,4 +295,90 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { server_registration: registration.serialize().to_vec(), }) } + + fn start_client_login(&self, password: &str) -> Result { + let mut client_rng = OsRng; + let result = ClientLogin::::start(&mut client_rng, password.as_bytes())?; + Ok(types::ClientLoginStartResult { + credential_request: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) + } + + fn start_server_login( + &self, + server_setup: &[u8], + server_registration: &[u8], + credential_request: &[u8], + + username: &str, + ) -> Result { + let mut server_rng = OsRng; + + let result = ServerLogin::start( + &mut server_rng, + &ServerSetup::::deserialize(server_setup)?, + Some(ServerRegistration::::deserialize( + server_registration, + )?), + CredentialRequest::deserialize(credential_request)?, + username.as_bytes(), + ServerLoginStartParameters::default(), + )?; + Ok(types::ServerLoginStartResult { + credential_response: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) + } + + fn finish_client_login( + &self, + state: &[u8], + credential_response: &[u8], + password: &str, + ) -> Result { + let client_login = ClientLogin::::deserialize(state)?; + let result = client_login.finish( + password.as_bytes(), + CredentialResponse::deserialize(credential_response)?, + ClientLoginFinishParameters::new( + None, + Identifiers::default(), + Some(&Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new( + self.0.memory_kib, + self.0.iterations, + self.0.parallelism, + None, + ) + .map_err(|_| Error::InvalidInput("Invalid Argon2 parameters"))?, + )), + ), + )?; + + Ok(types::ClientLoginFinishResult { + credential_finalization: result.message.serialize().to_vec(), + session_key: result.session_key.to_vec(), + export_key: result.export_key.to_vec(), + server_s_pk: result.server_s_pk.serialize().to_vec(), + }) + } + + fn finish_server_login( + &self, + state: &[u8], + credential_finalization: &[u8], + ) -> Result { + let server_login = ServerLogin::::deserialize(state)?; + + let result = server_login.finish(CredentialFinalization::deserialize( + credential_finalization, + )?)?; + + Ok(types::ServerLoginFinishResult { + session_key: result.session_key.to_vec(), + }) + } } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs index ee4092e1..14ce2926 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs @@ -70,3 +70,26 @@ pub(crate) struct ServerRegistrationStartResult { pub(crate) struct ServerRegistrationFinishResult { pub(crate) server_registration: Vec, } + +//////////////////////////////////////// + +pub(crate) struct ClientLoginStartResult { + pub(crate) credential_request: Vec, + pub(crate) state: Vec, +} + +pub(crate) struct ClientLoginFinishResult { + pub(crate) credential_finalization: Vec, + pub(crate) session_key: Vec, + pub(crate) export_key: Vec, + pub(crate) server_s_pk: Vec, +} + +pub(crate) struct ServerLoginStartResult { + pub(crate) credential_response: Vec, + pub(crate) state: Vec, +} + +pub(crate) struct ServerLoginFinishResult { + pub(crate) session_key: Vec, +} From 68146e21f000aafc8a25697c2bd447f18165fbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 18:30:59 +0100 Subject: [PATCH 45/89] Implement login --- .../Bitwarden.OPAQUE/rust/src/ffi/mod.rs | 149 ++++++++++++++++-- .../Bitwarden.OPAQUE/rust/src/ffi/types.rs | 17 ++ .../Bitwarden.OPAQUE/rust/src/opaque/types.rs | 20 +-- .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 98 ++++++++++-- .../src/BitwardenOpaqueClient.cs | 38 +++++ .../src/BitwardenOpaqueServer.cs | 31 ++++ extensions/Bitwarden.OPAQUE/tests/Tests.cs | 29 +++- 7 files changed, 345 insertions(+), 37 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs index d3fe273b..23d65868 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs @@ -19,6 +19,31 @@ unsafe fn handle_string_input<'a>( Err(_) => Err(Response::error(Error::InvalidInput(name))), } } +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> Response { + let password = match unsafe { handle_string_input(password, "password") } { + Ok(s) => s, + Err(e) => { + return e; + } + }; + + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let result = match config.start_client_registration(password) { + Ok(result) => result, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok2(result.registration_request, result.state) +} /// /// @@ -53,6 +78,43 @@ pub unsafe extern "C" fn start_server_registration( Response::ok2(response.registration_response, response.server_setup) } +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn finish_client_registration( + state: Buffer, + registration_response: Buffer, + password: *const c_char, +) -> Response { + let registration_response = unsafe { registration_response.as_slice() }; + let state = unsafe { state.as_slice() }; + + let password = match unsafe { handle_string_input(password, "password") } { + Ok(s) => s, + Err(e) => { + return e; + } + }; + + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let response = match config.finish_client_registration(state, registration_response, password) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok3( + response.registration_upload, + response.export_key, + response.server_s_pk, + ) +} + /// /// /// # Safety @@ -79,7 +141,7 @@ pub unsafe extern "C" fn finish_server_registration(registration_upload: Buffer) /// # Safety /// ABC #[unsafe(no_mangle)] -pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> Response { +pub unsafe extern "C" fn start_client_login(password: *const c_char) -> Response { let password = match unsafe { handle_string_input(password, "password") } { Ok(s) => s, Err(e) => { @@ -90,14 +152,14 @@ pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> R // TODO: Allow configuring the ciphers let config = CipherConfiguration::default(); - let result = match config.start_client_registration(password) { - Ok(result) => result, + let response = match config.start_client_login(password) { + Ok(response) => response, Err(e) => { return Response::error(e); } }; - Response::ok2(result.registration_request, result.state) + Response::ok2(response.credential_request, response.state) } /// @@ -105,14 +167,53 @@ pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> R /// # Safety /// ABC #[unsafe(no_mangle)] -pub unsafe extern "C" fn finish_client_registration( +pub unsafe extern "C" fn start_server_login( + server_setup: Buffer, + server_registration: Buffer, + credential_request: Buffer, + username: *const c_char, +) -> Response { + let server_setup = unsafe { server_setup.as_slice() }; + let server_registration = unsafe { server_registration.as_slice() }; + let credential_request = unsafe { credential_request.as_slice() }; + + let username = match unsafe { handle_string_input(username, "username") } { + Ok(s) => s, + Err(e) => { + return e; + } + }; + + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let response = match config.start_server_login( + server_setup, + server_registration, + credential_request, + username, + ) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok2(response.credential_response, response.state) +} + +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn finish_client_login( state: Buffer, - registration_response: Buffer, + credential_response: Buffer, password: *const c_char, ) -> Response { - let registration_response = unsafe { registration_response.as_slice() }; let state = unsafe { state.as_slice() }; - + let credential_response = unsafe { credential_response.as_slice() }; let password = match unsafe { handle_string_input(password, "password") } { Ok(s) => s, Err(e) => { @@ -123,16 +224,42 @@ pub unsafe extern "C" fn finish_client_registration( // TODO: Allow configuring the ciphers let config = CipherConfiguration::default(); - let response = match config.finish_client_registration(state, registration_response, password) { + let response = match config.finish_client_login(state, credential_response, password) { Ok(response) => response, Err(e) => { return Response::error(e); } }; - Response::ok3( - response.registration_upload, + Response::ok4( + response.credential_finalization, + response.session_key, response.export_key, response.server_s_pk, ) } + +/// +/// +/// # Safety +/// ABC +#[unsafe(no_mangle)] +pub unsafe extern "C" fn finish_server_login( + state: Buffer, + credential_finalization: Buffer, +) -> Response { + let state = unsafe { state.as_slice() }; + let credential_finalization = unsafe { credential_finalization.as_slice() }; + + // TODO: Allow configuring the ciphers + let config = CipherConfiguration::default(); + + let response = match config.finish_server_login(state, credential_finalization) { + Ok(response) => response, + Err(e) => { + return Response::error(e); + } + }; + + Response::ok1(response.session_key) +} diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs index 3ccb5351..913f56aa 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs @@ -67,6 +67,7 @@ pub struct Response { pub data1: Buffer, pub data2: Buffer, pub data3: Buffer, + pub data4: Buffer, } impl Response { @@ -78,6 +79,7 @@ impl Response { data1: Buffer::from_vec(data1), data2: Buffer::empty(), data3: Buffer::empty(), + data4: Buffer::empty(), } } @@ -89,6 +91,7 @@ impl Response { data1: Buffer::from_vec(data1), data2: Buffer::from_vec(data2), data3: Buffer::empty(), + data4: Buffer::empty(), } } @@ -100,6 +103,19 @@ impl Response { data1: Buffer::from_vec(data1), data2: Buffer::from_vec(data2), data3: Buffer::from_vec(data3), + data4: Buffer::empty(), + } + } + + pub fn ok4(data1: Vec, data2: Vec, data3: Vec, data4: Vec) -> Self { + Response { + error: 0, + error_message: Buffer::empty(), + + data1: Buffer::from_vec(data1), + data2: Buffer::from_vec(data2), + data3: Buffer::from_vec(data3), + data4: Buffer::from_vec(data4), } } @@ -117,6 +133,7 @@ impl Response { data1: Buffer::empty(), data2: Buffer::empty(), data3: Buffer::empty(), + data4: Buffer::empty(), } } } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs index 14ce2926..05833b8f 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs @@ -55,6 +55,11 @@ pub(crate) struct ClientRegistrationStartResult { pub(crate) state: Vec, } +pub(crate) struct ServerRegistrationStartResult { + pub(crate) registration_response: Vec, + pub(crate) server_setup: Vec, +} + pub(crate) struct ClientRegistrationFinishResult { // The message is sent to the server for the last step of the registration protocol. pub(crate) registration_upload: Vec, @@ -62,11 +67,6 @@ pub(crate) struct ClientRegistrationFinishResult { pub(crate) server_s_pk: Vec, } -pub(crate) struct ServerRegistrationStartResult { - pub(crate) registration_response: Vec, - pub(crate) server_setup: Vec, -} - pub(crate) struct ServerRegistrationFinishResult { pub(crate) server_registration: Vec, } @@ -78,6 +78,11 @@ pub(crate) struct ClientLoginStartResult { pub(crate) state: Vec, } +pub(crate) struct ServerLoginStartResult { + pub(crate) credential_response: Vec, + pub(crate) state: Vec, +} + pub(crate) struct ClientLoginFinishResult { pub(crate) credential_finalization: Vec, pub(crate) session_key: Vec, @@ -85,11 +90,6 @@ pub(crate) struct ClientLoginFinishResult { pub(crate) server_s_pk: Vec, } -pub(crate) struct ServerLoginStartResult { - pub(crate) credential_response: Vec, - pub(crate) state: Vec, -} - pub(crate) struct ServerLoginFinishResult { pub(crate) session_key: Vec, } diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs index e1d0fff7..ee27cc9a 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -32,23 +32,36 @@ private struct Response public Buffer data1; public Buffer data2; public Buffer data3; + public Buffer data4; } [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial void free_buffer(Buffer buf); + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response start_client_registration(string password); + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial Response start_server_registration(Buffer server_setup, Buffer registration_request, string username); + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response finish_client_registration(Buffer state, Buffer registration_response, string password); + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial Response finish_server_registration(Buffer registration_upload); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_client_registration(string password); + private static partial Response start_client_login(string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_client_registration(Buffer state, Buffer registration_response, string password); + private static partial Response start_server_login(Buffer server_setup, Buffer server_registration, Buffer credential_request, string username); + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response finish_client_login(Buffer state, Buffer credential_response, string password); + + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response finish_server_login(Buffer state, Buffer credential_finalization); private static Buffer BuildBuffer(byte[]? data, out GCHandle handle) { @@ -83,7 +96,7 @@ private static List HandleResponse(Response response, int expectedValues } // If we don't receive an error, parse all the return types - var buffers = new Buffer[] { response.data1, response.data2, response.data3 }; + var buffers = new Buffer[] { response.data1, response.data2, response.data3, response.data4 }; var arrays = new List { }; foreach (var buffer in buffers) @@ -100,6 +113,13 @@ private static List HandleResponse(Response response, int expectedValues return arrays; } + internal static (byte[], byte[]) StartClientRegistration(string password) + { + var response = start_client_registration(password); + var ret = HandleResponse(response, 2); + return (ret[0], ret[1]); + } + internal static (byte[], byte[]) StartServerRegistration(byte[]? serverSetup, byte[] registrationRequest, string username) { var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); @@ -118,6 +138,24 @@ internal static (byte[], byte[]) StartServerRegistration(byte[]? serverSetup, by } + internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] state, byte[] registrationResponse, string password) + { + var stateBuf = BuildBuffer(state, out var stateHandle); + var registrationResponseBuf = BuildBuffer(registrationResponse, out var registrationResponseHandle); + + try + { + var response = finish_client_registration(stateBuf, registrationResponseBuf, password); + var ret = HandleResponse(response, 3); + return (ret[0], ret[1], ret[2]); + } + finally + { + stateHandle.Free(); + registrationResponseHandle.Free(); + } + } + internal static byte[] FinishServerRegistration(byte[] registrationUpload) { var registrationUploadBuf = BuildBuffer(registrationUpload, out var handle); @@ -133,28 +171,66 @@ internal static byte[] FinishServerRegistration(byte[] registrationUpload) } - internal static (byte[], byte[]) StartClientRegistration(string password) + internal static (byte[], byte[]) StartClientLogin(string password) { - var response = start_client_registration(password); + var response = start_client_login(password); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); } - internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] state, byte[] registrationResponse, string password) + internal static (byte[], byte[]) StartServerLogin(byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) + { + var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); + var serverRegistrationBuf = BuildBuffer(serverRegistration, out var serverRegistrationHandle); + var credentialRequestBuf = BuildBuffer(credentialRequest, out var credentialRequestHandle); + try + { + var response = start_server_login(serverSetupBuf, serverRegistrationBuf, credentialRequestBuf, username); + var ret = HandleResponse(response, 2); + return (ret[0], ret[1]); + } + finally + { + serverSetupHandle.Free(); + serverRegistrationHandle.Free(); + credentialRequestHandle.Free(); + } + + } + + internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(byte[] state, byte[] credentialResponse, string password) { var stateBuf = BuildBuffer(state, out var stateHandle); - var registrationResponseBuf = BuildBuffer(registrationResponse, out var registrationResponseHandle); + var credentialResponseBuf = BuildBuffer(credentialResponse, out var credentialResponseHandle); try { - var response = finish_client_registration(stateBuf, registrationResponseBuf, password); - var ret = HandleResponse(response, 3); - return (ret[0], ret[1], ret[2]); + var response = finish_client_login(stateBuf, credentialResponseBuf, password); + var ret = HandleResponse(response, 4); + return (ret[0], ret[1], ret[2], ret[3]); } finally { stateHandle.Free(); - registrationResponseHandle.Free(); + credentialResponseHandle.Free(); + } + } + + internal static byte[] FinishServerLogin(byte[] state, byte[] credentialFinalization) + { + var stateBuf = BuildBuffer(state, out var stateHandle); + var credentialFinalizationBuf = BuildBuffer(credentialFinalization, out var credentialFinalizationHandle); + try + { + var response = finish_server_login(stateBuf, credentialFinalizationBuf); + return HandleResponse(response, 1)[0]; } + finally + { + stateHandle.Free(); + credentialFinalizationHandle.Free(); + } + } + } diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs index b78252a4..52d9daec 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs @@ -22,6 +22,20 @@ public struct ClientRegistrationFinishResult public byte[] serverSPKey; } +public struct ClientLoginStartResult +{ + public byte[] credentialRequest; + public byte[] state; +} + +public struct ClientLoginFinishResult +{ + public byte[] credentialFinalization; + public byte[] sessionKey; + public byte[] exportKey; + public byte[] serverSPKey; +} + /// A class to represent client side functionality the Bitwarden OPAQUE library. public sealed partial class BitwardenOpaqueClient { @@ -60,4 +74,28 @@ public ClientRegistrationFinishResult FinishRegistration(CipherConfiguration con serverSPKey = serverSPKey }; } + + + public ClientLoginStartResult StartLogin(CipherConfiguration config, string password) + { + var (credentialRequest, state) = BitwardenLibrary.StartClientLogin(password); + return new ClientLoginStartResult + { + credentialRequest = credentialRequest, + state = state + }; + + } + + public ClientLoginFinishResult FinishLogin(CipherConfiguration config, byte[] state, byte[] credentialResponse, string password) + { + var (credentialFinalization, sessionKey, exportKey, serverSPKey) = BitwardenLibrary.FinishClientLogin(state, credentialResponse, password); + return new ClientLoginFinishResult + { + credentialFinalization = credentialFinalization, + sessionKey = sessionKey, + exportKey = exportKey, + serverSPKey = serverSPKey + }; + } } diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs index a966cdec..2e21d9dc 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs @@ -19,6 +19,18 @@ public struct ServerRegistrationFinishResult public byte[] serverRegistration; } +public struct ServerLoginStartResult +{ + public byte[] credentialResponse; + public byte[] state; +} + +public struct ServerLoginFinishResult +{ + public byte[] sessionKey; + +} + /// A class to represent server side functionality the Bitwarden OPAQUE library. public sealed partial class BitwardenOpaqueServer { @@ -55,4 +67,23 @@ public ServerRegistrationFinishResult FinishRegistration(CipherConfiguration con }; } + public ServerLoginStartResult StartLogin(CipherConfiguration config, byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) + { + var (credentialResponse, state) = BitwardenLibrary.StartServerLogin(serverSetup, serverRegistration, credentialRequest, username); + return new ServerLoginStartResult + { + credentialResponse = credentialResponse, + state = state + }; + } + + public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] state, byte[] credentialFinalization) + { + var sessionKey = BitwardenLibrary.FinishServerLogin(state, credentialFinalization); + return new ServerLoginFinishResult + { + sessionKey = sessionKey + }; + } + } diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index b723ee90..e6fa9e07 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -17,18 +17,37 @@ public void TestRegistration() var config = CipherConfiguration.Default; + ///// Registration + // Start the client registration - var clientStartResult = client.StartRegistration(config, password); + var clientRegisterStartResult = client.StartRegistration(config, password); // Client sends reg_start to server - var serverStartResult = server.StartRegistration(config, null, clientStartResult.registrationRequest, username); + var serverRegisterStartResult = server.StartRegistration(config, null, clientRegisterStartResult.registrationRequest, username); // Server sends server_start_result to client - var clientFinishResult = client.FinishRegistration(config, clientStartResult.state, serverStartResult.registrationResponse, password); + var clientRegisterFinishResult = client.FinishRegistration(config, clientRegisterStartResult.state, serverRegisterStartResult.registrationResponse, password); // Client sends client_finish_result to server - var serverFinishResult = server.FinishRegistration(config, clientFinishResult.registrationUpload); + var serverRegisterFinishResult = server.FinishRegistration(config, clientRegisterFinishResult.registrationUpload); + + Assert.NotNull(serverRegisterFinishResult.serverRegistration); + + ///// Login + + // Start the client login + var clientLoginStartResult = client.StartLogin(config, password); + + // Client sends login_start to server + var serverLoginStartResult = server.StartLogin(config, serverRegisterStartResult.serverSetup, serverRegisterFinishResult.serverRegistration, clientLoginStartResult.credentialRequest, username); + + // Server sends login_start_result to client + var clientLoginFinishResult = client.FinishLogin(config, clientLoginStartResult.state, serverLoginStartResult.credentialResponse, password); + + // Client sends login_finish_result to server + var serverLoginFinishResult = server.FinishLogin(config, serverLoginStartResult.state, clientLoginFinishResult.credentialFinalization); + + Assert.NotNull(serverLoginFinishResult.sessionKey); - Assert.NotNull(serverFinishResult.serverRegistration); } } From c217c7fb1fd5b621f437ab1d0ad81fb233c4a69c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 19:05:38 +0100 Subject: [PATCH 46/89] Extract KSF trait --- .../Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs index f872ed75..ae51ff60 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -54,6 +54,11 @@ pub trait OpaqueImpl { ) -> Result; } +pub trait OpaqueKsf { + type Output; + fn get_ksf(&self) -> Result; +} + // Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. #[allow(unreachable_patterns)] impl OpaqueImpl for CipherConfiguration { @@ -210,6 +215,24 @@ impl OpaqueImpl for CipherConfiguration { // we will need to duplicate most of this code. It should be entirely the same, // with the exception of the KDF settings, so we should build a macro for that struct RistrettoTripleDhArgonSuite(Argon2id); + +impl OpaqueKsf for RistrettoTripleDhArgonSuite { + type Output = argon2::Argon2<'static>; + fn get_ksf(&self) -> Result { + Ok(Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new( + self.0.memory_kib, + self.0.iterations, + self.0.parallelism, + None, + ) + .map_err(|_| Error::InvalidInput("Invalid Argon2 parameters"))?, + )) + } +} + impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { type OprfCs = opaque_ke::Ristretto255; type KeGroup = opaque_ke::Ristretto255; @@ -261,20 +284,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { &mut OsRng, password.as_bytes(), RegistrationResponse::deserialize(registration_response)?, - ClientRegistrationFinishParameters::new( - Identifiers::default(), - Some(&Argon2::new( - argon2::Algorithm::Argon2id, - argon2::Version::V0x13, - argon2::Params::new( - self.0.memory_kib, - self.0.iterations, - self.0.parallelism, - None, - ) - .map_err(|_| Error::InvalidInput("Invalid Argon2 parameters"))?, - )), - ), + ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&self.get_ksf()?)), )?; Ok(types::ClientRegistrationFinishResult { @@ -341,21 +351,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { let result = client_login.finish( password.as_bytes(), CredentialResponse::deserialize(credential_response)?, - ClientLoginFinishParameters::new( - None, - Identifiers::default(), - Some(&Argon2::new( - argon2::Algorithm::Argon2id, - argon2::Version::V0x13, - argon2::Params::new( - self.0.memory_kib, - self.0.iterations, - self.0.parallelism, - None, - ) - .map_err(|_| Error::InvalidInput("Invalid Argon2 parameters"))?, - )), - ), + ClientLoginFinishParameters::new(None, Identifiers::default(), Some(&self.get_ksf()?)), )?; Ok(types::ClientLoginFinishResult { From f0e8d17584def5c938c47a501f5e074e1667c756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 19:33:16 +0100 Subject: [PATCH 47/89] Add possible idea for a future macro so I don't forget --- .../Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs index ae51ff60..6398b578 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -213,7 +213,8 @@ impl OpaqueImpl for CipherConfiguration { // Define the cipher suite and implement OpaqueImpl for it. // Note that in the future if we want to support multiple cipher suites, // we will need to duplicate most of this code. It should be entirely the same, -// with the exception of the KDF settings, so we should build a macro for that +// with the exception of the KDF settings, so we should build a macro for that. +// There is an example of this macro at the bottom of this file. struct RistrettoTripleDhArgonSuite(Argon2id); impl OpaqueKsf for RistrettoTripleDhArgonSuite { @@ -378,3 +379,51 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } } + +/* + TODO: If in the future we add more cipher suites, we will need to create + a new OpaqueImpl implementation and add it to the CipherConfiguration matches. + This will require a lot of duplication, but can be simplified with a macro as follows: + + macro_rules! implement_cipher_suites { + ( $shared_type:ident; $( $name:ident : $pat:pat => $cipher:expr );+ ) => { + // Check that any type implements the required traits + const fn _assert_opaque_ksf_and_cipher_suite() {} + const _: () = { $( _assert_opaque_ksf_and_cipher_suite::<$name>(); )+ }; + + // Implement OpaqueImpl for the shared type, and dispatch to the correct cipher suite + impl OpaqueImpl for $shared_type { + fn start_client_registration(&self, password: &str) -> Result { + match self { + $( $pat => $cipher.start_client_registration(password), )+ + _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } + } + ... + } + + // Implement OpaqueImpl for each cipher suite. + // This is just copying the implementation on RistrettoTripleDhArgonSuite and wrapping it in $()+ + $(impl OpaqueImpl for $name { + fn start_client_registration(&self, password: &str) -> Result { + let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; + Ok(types::ClientRegistrationStartResult { + registration_request: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) + } + ... + })+ + }; + } + + implement_cipher_suites! { + CipherConfiguration; + RistrettoTripleDhArgonSuite: CipherConfiguration { + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: Ksf::Argon2id(argon), + } => RistrettoTripleDhArgonSuite(*argon) + } +*/ From bdca32172a132ff0e0dbad31f1a4082957e74708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 19:50:50 +0100 Subject: [PATCH 48/89] Simplify code a bit with some util trait --- .../Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 192 ++++++------------ .../Bitwarden.OPAQUE/rust/src/opaque/types.rs | 1 + 2 files changed, 64 insertions(+), 129 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs index 6398b578..799c4274 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -46,7 +46,6 @@ pub trait OpaqueImpl { credential_response: &[u8], password: &str, ) -> Result; - fn finish_server_login( &self, state: &[u8], @@ -54,100 +53,63 @@ pub trait OpaqueImpl { ) -> Result; } -pub trait OpaqueKsf { +// This trait exists to extract the differences between all the OpaqueImpl implementations. +// This would allow replacing those impls by a macro in the future. +pub trait OpaqueUtil: Sized { type Output; + fn as_variant(config: &CipherConfiguration) -> Option; fn get_ksf(&self) -> Result; } // Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. -#[allow(unreachable_patterns)] impl OpaqueImpl for CipherConfiguration { fn start_client_registration( &self, password: &str, ) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).start_client_registration(password), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.start_client_registration(password); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } - fn start_server_registration( &self, server_setup: Option<&[u8]>, registration_request: &[u8], username: &str, ) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).start_server_registration( - server_setup, - registration_request, - username, - ), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.start_server_registration(server_setup, registration_request, username); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } - fn finish_client_registration( &self, state: &[u8], registration_response: &[u8], password: &str, ) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).finish_client_registration( - state, - registration_response, - password, - ), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.finish_client_registration(state, registration_response, password); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } - fn finish_server_registration( &self, registration_upload: &[u8], ) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => { - RistrettoTripleDhArgonSuite(*argon).finish_server_registration(registration_upload) - } - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.finish_server_registration(registration_upload); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } fn start_client_login(&self, password: &str) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).start_client_login(password), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.start_client_login(password); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } - fn start_server_login( &self, server_setup: &[u8], @@ -155,70 +117,61 @@ impl OpaqueImpl for CipherConfiguration { credential_request: &[u8], username: &str, ) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).start_server_login( + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.start_server_login( server_setup, server_registration, credential_request, username, - ), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + ); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } - fn finish_client_login( &self, state: &[u8], credential_response: &[u8], password: &str, ) -> Result { - match self { - CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon).finish_client_login( - state, - credential_response, - password, - ), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.finish_client_login(state, credential_response, password); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) } - fn finish_server_login( &self, state: &[u8], credential_finalization: &[u8], ) -> Result { - match self { + if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + return suite.finish_server_login(state, credential_finalization); + }; + Err(Error::InvalidInput("Invalid cipher configuration")) + } +} + +// Define the cipher suite and implement the required traits on it (OpaqueImpl+OpaqueKsf+opaque_ke::CipherSuite) +struct RistrettoTripleDhArgonSuite(Argon2id); +impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { + type OprfCs = opaque_ke::Ristretto255; + type KeGroup = opaque_ke::Ristretto255; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; + type Ksf = argon2::Argon2<'static>; +} +impl OpaqueUtil for RistrettoTripleDhArgonSuite { + type Output = argon2::Argon2<'static>; + + fn as_variant(config: &CipherConfiguration) -> Option { + match config { CipherConfiguration { oprf_cs: OprfCs::Ristretto255, ke_group: KeGroup::Ristretto255, key_exchange: KeyExchange::TripleDh, ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon) - .finish_server_login(state, credential_finalization), - _ => Err(Error::InvalidInput("Invalid cipher configuration")), + } => Some(Self(*argon)), + _ => None, } } -} - -// Define the cipher suite and implement OpaqueImpl for it. -// Note that in the future if we want to support multiple cipher suites, -// we will need to duplicate most of this code. It should be entirely the same, -// with the exception of the KDF settings, so we should build a macro for that. -// There is an example of this macro at the bottom of this file. -struct RistrettoTripleDhArgonSuite(Argon2id); - -impl OpaqueKsf for RistrettoTripleDhArgonSuite { - type Output = argon2::Argon2<'static>; fn get_ksf(&self) -> Result { Ok(Argon2::new( argon2::Algorithm::Argon2id, @@ -234,13 +187,8 @@ impl OpaqueKsf for RistrettoTripleDhArgonSuite { } } -impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { - type OprfCs = opaque_ke::Ristretto255; - type KeGroup = opaque_ke::Ristretto255; - type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; - type Ksf = argon2::Argon2<'static>; -} - +// This implementation will be identical between any cipher suite, but we can't simply reuse it because of all the generic bounds on the CipherSuite trait. +// If we need to add more cipher suites, we will need to copy this implementation over, or ideally use a macro to generate it. impl OpaqueImpl for RistrettoTripleDhArgonSuite { fn start_client_registration( &self, @@ -252,7 +200,6 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { state: result.state.serialize().to_vec(), }) } - fn start_server_registration( &self, server_setup: Option<&[u8]>, @@ -273,7 +220,6 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { server_setup: server_setup.serialize().to_vec(), }) } - fn finish_client_registration( &self, state: &[u8], @@ -287,14 +233,12 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { RegistrationResponse::deserialize(registration_response)?, ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&self.get_ksf()?)), )?; - Ok(types::ClientRegistrationFinishResult { registration_upload: result.message.serialize().to_vec(), export_key: result.export_key.to_vec(), server_s_pk: result.server_s_pk.serialize().to_vec(), }) } - fn finish_server_registration( &self, registration_upload: &[u8], @@ -308,26 +252,21 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { } fn start_client_login(&self, password: &str) -> Result { - let mut client_rng = OsRng; - let result = ClientLogin::::start(&mut client_rng, password.as_bytes())?; + let result = ClientLogin::::start(&mut OsRng, password.as_bytes())?; Ok(types::ClientLoginStartResult { credential_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) } - fn start_server_login( &self, server_setup: &[u8], server_registration: &[u8], credential_request: &[u8], - username: &str, ) -> Result { - let mut server_rng = OsRng; - let result = ServerLogin::start( - &mut server_rng, + &mut OsRng, &ServerSetup::::deserialize(server_setup)?, Some(ServerRegistration::::deserialize( server_registration, @@ -341,7 +280,6 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { state: result.state.serialize().to_vec(), }) } - fn finish_client_login( &self, state: &[u8], @@ -354,7 +292,6 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { CredentialResponse::deserialize(credential_response)?, ClientLoginFinishParameters::new(None, Identifiers::default(), Some(&self.get_ksf()?)), )?; - Ok(types::ClientLoginFinishResult { credential_finalization: result.message.serialize().to_vec(), session_key: result.session_key.to_vec(), @@ -362,18 +299,15 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { server_s_pk: result.server_s_pk.serialize().to_vec(), }) } - fn finish_server_login( &self, state: &[u8], credential_finalization: &[u8], ) -> Result { let server_login = ServerLogin::::deserialize(state)?; - let result = server_login.finish(CredentialFinalization::deserialize( credential_finalization, )?)?; - Ok(types::ServerLoginFinishResult { session_key: result.session_key.to_vec(), }) @@ -394,10 +328,10 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { // Implement OpaqueImpl for the shared type, and dispatch to the correct cipher suite impl OpaqueImpl for $shared_type { fn start_client_registration(&self, password: &str) -> Result { - match self { - $( $pat => $cipher.start_client_registration(password), )+ - _ => Err(Error::InvalidInput("Invalid cipher configuration")), - } + $(if let Some(suite) = $name::as_variant(self) { + return suite.start_client_registration(password); + };)+ + Err(Error::InvalidInput("Invalid cipher configuration")) } ... } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs index 05833b8f..616dcd71 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs @@ -16,6 +16,7 @@ pub enum KeyExchange { #[derive(Debug, Clone, Copy)] pub enum Ksf { Argon2id(Argon2id), + __NonExhaustive(()), } #[derive(Debug, Clone, Copy)] From 218d26cc59e93ba95c318be84517cd14e681706b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 12 Mar 2025 19:57:41 +0100 Subject: [PATCH 49/89] Fix comment --- extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs index 799c4274..cdd9f44c 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -150,7 +150,7 @@ impl OpaqueImpl for CipherConfiguration { } } -// Define the cipher suite and implement the required traits on it (OpaqueImpl+OpaqueKsf+opaque_ke::CipherSuite) +// Define the cipher suite and implement the required traits on it (opaque_ke::CipherSuite+OpaqueUtil+OpaqueImpl) struct RistrettoTripleDhArgonSuite(Argon2id); impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { type OprfCs = opaque_ke::Ristretto255; From b2d8e89c9f31fa3c313351b49935b7d768267909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 13 Mar 2025 11:48:32 +0100 Subject: [PATCH 50/89] Fix package path on debug mode --- extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index e6c6e21b..588e7b7b 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -43,8 +43,8 @@ ../rust/target/debug/ + $(NETCoreSdkRuntimeIdentifier) - opaque_ke_binding.dll @@ -66,6 +66,7 @@ Always true + runtimes/$(CurrentRID)/native From 5142a6c5c3e52544b76498746b05b47226ac4ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 13 Mar 2025 16:14:38 +0100 Subject: [PATCH 51/89] Simplify FFI parsing with a macro, fail on null buffers, properly pass config --- extensions/Bitwarden.OPAQUE/rust/Cargo.lock | 44 +++- extensions/Bitwarden.OPAQUE/rust/Cargo.toml | 2 + .../Bitwarden.OPAQUE/rust/src/ffi/mod.rs | 216 ++++++------------ .../Bitwarden.OPAQUE/rust/src/ffi/types.rs | 69 +++--- extensions/Bitwarden.OPAQUE/rust/src/lib.rs | 3 +- .../Bitwarden.OPAQUE/rust/src/opaque/mod.rs | 32 +-- .../Bitwarden.OPAQUE/rust/src/opaque/types.rs | 44 +++- .../src/Bitwarden.OPAQUE.csproj | 7 +- .../src/BitwardenException.cs | 3 +- .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 67 +++--- .../src/BitwardenOpaqueClient.cs | 8 +- .../src/BitwardenOpaqueServer.cs | 8 +- .../src/CipherConfiguration.cs | 50 +++- extensions/Bitwarden.OPAQUE/tests/Tests.cs | 19 +- 14 files changed, 320 insertions(+), 252 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.lock b/extensions/Bitwarden.OPAQUE/rust/Cargo.lock index 2c7dbce5..ca8596be 100644 --- a/extensions/Bitwarden.OPAQUE/rust/Cargo.lock +++ b/extensions/Bitwarden.OPAQUE/rust/Cargo.lock @@ -22,9 +22,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" [[package]] name = "blake2" @@ -251,6 +251,12 @@ dependencies = [ "digest", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -263,9 +269,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.170" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "log" @@ -273,6 +279,12 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "once_cell" version = "1.21.0" @@ -311,6 +323,8 @@ dependencies = [ "generic-array", "opaque-ke", "rand", + "serde", + "serde_json", "voprf", "zeroizing-alloc", ] @@ -346,9 +360,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -392,6 +406,12 @@ dependencies = [ "semver", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "sec1" version = "0.7.3" @@ -431,6 +451,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml index 4e55f27d..7a7f1fa7 100644 --- a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml +++ b/extensions/Bitwarden.OPAQUE/rust/Cargo.toml @@ -13,6 +13,8 @@ digest = "0.10.7" generic-array = "0.14.7" opaque-ke = { version = "3.0.0", features = ["std", "argon2", "curve25519"] } rand = "0.8.5" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" voprf = "0.5.0" zeroizing-alloc = "0.1.0" diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs index 23d65868..017755f3 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs @@ -1,46 +1,34 @@ -use std::ffi::c_char; +use std::{ffi::c_char, str::FromStr}; use crate::{ Error, opaque::{CipherConfiguration, OpaqueImpl}, + try_ffi, }; mod types; use types::*; -unsafe fn handle_string_input<'a>( - input: *const c_char, - name: &'static str, -) -> Result<&'a str, Response> { - let input = unsafe { std::ffi::CStr::from_ptr(input).to_str() }; - match input { - Ok(input) => Ok(input), - Err(_) => Err(Response::error(Error::InvalidInput(name))), - } +unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a str, Error> { + unsafe { std::ffi::CStr::from_ptr(input).to_str() }.map_err(|_| Error::InvalidInput(name.into())) } + /// /// /// # Safety /// ABC #[unsafe(no_mangle)] -pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> Response { - let password = match unsafe { handle_string_input(password, "password") } { - Ok(s) => s, - Err(e) => { - return e; - } - }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); - - let result = match config.start_client_registration(password) { - Ok(result) => result, - Err(e) => { - return Response::error(e); - } - }; +pub unsafe extern "C" fn start_client_registration( + config: *const c_char, + password: *const c_char, +) -> Response { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let password: &str = try_ffi!(unsafe { parse_str(password, "password") }); + + let config = try_ffi!(CipherConfiguration::from_str(config)); + + let result = try_ffi!(config.start_client_registration(password)); Response::ok2(result.registration_request, result.state) } @@ -51,29 +39,20 @@ pub unsafe extern "C" fn start_client_registration(password: *const c_char) -> R /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_registration( + config: *const c_char, server_setup: Buffer, registration_request: Buffer, username: *const c_char, ) -> Response { - let server_setup = unsafe { server_setup.as_slice2() }.ok(); - let registration_request = unsafe { registration_request.as_slice() }; - let username = match unsafe { handle_string_input(username, "username") } { - Ok(s) => s, - Err(e) => { - return e; - } - }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let server_setup = unsafe { server_setup.as_slice() }.ok(); + let registration_request = try_ffi!(unsafe { registration_request.as_slice() }); + let username = try_ffi!(unsafe { parse_str(username, "username") }); + + let config = try_ffi!(CipherConfiguration::from_str(config)); let response = - match config.start_server_registration(server_setup, registration_request, username) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; + try_ffi!(config.start_server_registration(server_setup, registration_request, username)); Response::ok2(response.registration_response, response.server_setup) } @@ -84,29 +63,20 @@ pub unsafe extern "C" fn start_server_registration( /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_registration( + config: *const c_char, state: Buffer, registration_response: Buffer, password: *const c_char, ) -> Response { - let registration_response = unsafe { registration_response.as_slice() }; - let state = unsafe { state.as_slice() }; - - let password = match unsafe { handle_string_input(password, "password") } { - Ok(s) => s, - Err(e) => { - return e; - } - }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); - - let response = match config.finish_client_registration(state, registration_response, password) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let registration_response = try_ffi!(unsafe { registration_response.as_slice() }); + let state = try_ffi!(unsafe { state.as_slice() }); + let password = try_ffi!(unsafe { parse_str(password, "password") }); + + let config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = + try_ffi!(config.finish_client_registration(state, registration_response, password)); Response::ok3( response.registration_upload, @@ -120,19 +90,16 @@ pub unsafe extern "C" fn finish_client_registration( /// # Safety /// ABC #[unsafe(no_mangle)] -pub unsafe extern "C" fn finish_server_registration(registration_upload: Buffer) -> Response { - let registration_upload = unsafe { registration_upload.as_slice() }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); +pub unsafe extern "C" fn finish_server_registration( + config: *const c_char, + registration_upload: Buffer, +) -> Response { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let registration_upload = try_ffi!(unsafe { registration_upload.as_slice() }); - let response = match config.finish_server_registration(registration_upload) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; + let config = try_ffi!(CipherConfiguration::from_str(config)); + let response = try_ffi!(config.finish_server_registration(registration_upload)); Response::ok1(response.server_registration) } @@ -141,24 +108,16 @@ pub unsafe extern "C" fn finish_server_registration(registration_upload: Buffer) /// # Safety /// ABC #[unsafe(no_mangle)] -pub unsafe extern "C" fn start_client_login(password: *const c_char) -> Response { - let password = match unsafe { handle_string_input(password, "password") } { - Ok(s) => s, - Err(e) => { - return e; - } - }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); - - let response = match config.start_client_login(password) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; +pub unsafe extern "C" fn start_client_login( + config: *const c_char, + password: *const c_char, +) -> Response { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let password = try_ffi!(unsafe { parse_str(password, "password") }); + let config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = try_ffi!(config.start_client_login(password)); Response::ok2(response.credential_request, response.state) } @@ -168,37 +127,26 @@ pub unsafe extern "C" fn start_client_login(password: *const c_char) -> Response /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_login( + config: *const c_char, server_setup: Buffer, server_registration: Buffer, credential_request: Buffer, username: *const c_char, ) -> Response { - let server_setup = unsafe { server_setup.as_slice() }; - let server_registration = unsafe { server_registration.as_slice() }; - let credential_request = unsafe { credential_request.as_slice() }; - - let username = match unsafe { handle_string_input(username, "username") } { - Ok(s) => s, - Err(e) => { - return e; - } - }; + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let server_setup = try_ffi!(unsafe { server_setup.as_slice() }); + let server_registration = try_ffi!(unsafe { server_registration.as_slice() }); + let credential_request = try_ffi!(unsafe { credential_request.as_slice() }); + let username = try_ffi!(unsafe { parse_str(username, "username") }); - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); + let config = try_ffi!(CipherConfiguration::from_str(config)); - let response = match config.start_server_login( + let response = try_ffi!(config.start_server_login( server_setup, server_registration, credential_request, username, - ) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; - + )); Response::ok2(response.credential_response, response.state) } @@ -208,29 +156,19 @@ pub unsafe extern "C" fn start_server_login( /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_login( + config: *const c_char, state: Buffer, credential_response: Buffer, password: *const c_char, ) -> Response { - let state = unsafe { state.as_slice() }; - let credential_response = unsafe { credential_response.as_slice() }; - let password = match unsafe { handle_string_input(password, "password") } { - Ok(s) => s, - Err(e) => { - return e; - } - }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); - - let response = match config.finish_client_login(state, credential_response, password) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let state = try_ffi!(unsafe { state.as_slice() }); + let credential_response = try_ffi!(unsafe { credential_response.as_slice() }); + let password = try_ffi!(unsafe { parse_str(password, "password") }); + + let config = try_ffi!(CipherConfiguration::from_str(config)); + let response = try_ffi!(config.finish_client_login(state, credential_response, password)); Response::ok4( response.credential_finalization, response.session_key, @@ -245,21 +183,17 @@ pub unsafe extern "C" fn finish_client_login( /// ABC #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_server_login( + config: *const c_char, state: Buffer, credential_finalization: Buffer, ) -> Response { - let state = unsafe { state.as_slice() }; - let credential_finalization = unsafe { credential_finalization.as_slice() }; - - // TODO: Allow configuring the ciphers - let config = CipherConfiguration::default(); - - let response = match config.finish_server_login(state, credential_finalization) { - Ok(response) => response, - Err(e) => { - return Response::error(e); - } - }; + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let state = try_ffi!(unsafe { state.as_slice() }); + let credential_finalization = try_ffi!(unsafe { credential_finalization.as_slice() }); + + let config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = try_ffi!(config.finish_server_login(state, credential_finalization)); Response::ok1(response.session_key) } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs index 913f56aa..94be60ee 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs @@ -6,20 +6,20 @@ use crate::Error; /// The parameter should contain a Buffer pointing to valid initialized memory, or null. #[unsafe(no_mangle)] pub unsafe extern "C" fn free_buffer(buf: Buffer) { - buf.free(); + unsafe { buf.free() }; } #[repr(C)] pub struct Buffer { - pub data: *mut u8, - pub len: usize, + data: *mut u8, + len: usize, } /// A struct to represent a buffer of data. /// Important: The structure of this type must match the structure /// of the Buffer type in the C# BitwardenLibrary, both in field type and order. impl Buffer { - pub fn empty() -> Self { + pub fn null() -> Self { Buffer { data: std::ptr::null_mut(), len: 0, @@ -36,34 +36,40 @@ impl Buffer { Buffer { data, len } } - pub unsafe fn as_slice(&self) -> &[u8] { - unsafe { std::slice::from_raw_parts(self.data, self.len) } - } - - pub unsafe fn as_slice2(&self) -> Result<&[u8], Error> { + pub unsafe fn as_slice(&self) -> Result<&[u8], Error> { if self.data.is_null() { - return Err(Error::InvalidInput("Buffer data is null")); + return Err(Error::InvalidInput("Buffer data is null".into())); } Ok(unsafe { std::slice::from_raw_parts(self.data, self.len) }) } - pub fn free(self) { + pub unsafe fn free(self) { if !self.data.is_null() { let _ = unsafe { Vec::from_raw_parts(self.data, self.len, self.len) }; } } } -/// A struct to represent a response from the rust library. -/// Important: The structure of this type must match the structure -/// of the Response type in the C# BitwardenLibrary, both in field type and order. +#[macro_export] +macro_rules! try_ffi { + ($e:expr) => { + match $e { + Ok(v) => v, + Err(e) => return Response::error(e), + } + }; +} + +/// A struct to represent a response from the rust library. +/// Important: The structure of this type must match the structure of the +/// Response type in the C# BitwardenLibrary, both in field type and order. #[repr(C)] pub struct Response { pub error: usize, pub error_message: Buffer, // TODO: This is a way of returning multiple values without having different return FFI types. - // Ideally we'd have a separate type per return type? + // Ideally we'd have a separate type per return type? Or maybe a better way to represent this. pub data1: Buffer, pub data2: Buffer, pub data3: Buffer, @@ -74,43 +80,43 @@ impl Response { pub fn ok1(data1: Vec) -> Self { Response { error: 0, - error_message: Buffer::empty(), + error_message: Buffer::null(), data1: Buffer::from_vec(data1), - data2: Buffer::empty(), - data3: Buffer::empty(), - data4: Buffer::empty(), + data2: Buffer::null(), + data3: Buffer::null(), + data4: Buffer::null(), } } pub fn ok2(data1: Vec, data2: Vec) -> Self { Response { error: 0, - error_message: Buffer::empty(), + error_message: Buffer::null(), data1: Buffer::from_vec(data1), data2: Buffer::from_vec(data2), - data3: Buffer::empty(), - data4: Buffer::empty(), + data3: Buffer::null(), + data4: Buffer::null(), } } pub fn ok3(data1: Vec, data2: Vec, data3: Vec) -> Self { Response { error: 0, - error_message: Buffer::empty(), + error_message: Buffer::null(), data1: Buffer::from_vec(data1), data2: Buffer::from_vec(data2), data3: Buffer::from_vec(data3), - data4: Buffer::empty(), + data4: Buffer::null(), } } pub fn ok4(data1: Vec, data2: Vec, data3: Vec, data4: Vec) -> Self { Response { error: 0, - error_message: Buffer::empty(), + error_message: Buffer::null(), data1: Buffer::from_vec(data1), data2: Buffer::from_vec(data2), @@ -122,18 +128,19 @@ impl Response { pub fn error(error: Error) -> Self { // Important: The error codes need to be kept in sync with the BitwardenException in C#. let (error, message) = match error { - Error::InvalidInput(name) => (1, name.to_string()), - Error::Protocol(e) => (2, format!("{:?}", e)), + Error::InvalidInput(name) => (1, name), + Error::InvalidConfig(error) => (2, error), + Error::Protocol(e) => (3, format!("{:?}", e)), }; Response { error, error_message: Buffer::from_vec(message.into_bytes()), - data1: Buffer::empty(), - data2: Buffer::empty(), - data3: Buffer::empty(), - data4: Buffer::empty(), + data1: Buffer::null(), + data2: Buffer::null(), + data3: Buffer::null(), + data4: Buffer::null(), } } } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs index 60672a29..f9f09126 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/lib.rs @@ -10,7 +10,8 @@ mod opaque; #[derive(Debug)] pub enum Error { - InvalidInput(&'static str), + InvalidInput(String), + InvalidConfig(String), Protocol(opaque_ke::errors::ProtocolError), } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs index cdd9f44c..d95dc609 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs @@ -61,6 +61,10 @@ pub trait OpaqueUtil: Sized { fn get_ksf(&self) -> Result; } +fn invalid_config(config: &CipherConfiguration) -> Error { + Error::InvalidConfig(serde_json::to_string(config).unwrap_or_default()) +} + // Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. impl OpaqueImpl for CipherConfiguration { fn start_client_registration( @@ -70,7 +74,9 @@ impl OpaqueImpl for CipherConfiguration { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_client_registration(password); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(Error::InvalidConfig( + serde_json::to_string(self).unwrap_or_default(), + )) } fn start_server_registration( &self, @@ -81,7 +87,7 @@ impl OpaqueImpl for CipherConfiguration { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_server_registration(server_setup, registration_request, username); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } fn finish_client_registration( &self, @@ -92,7 +98,7 @@ impl OpaqueImpl for CipherConfiguration { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_client_registration(state, registration_response, password); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } fn finish_server_registration( &self, @@ -101,14 +107,14 @@ impl OpaqueImpl for CipherConfiguration { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_server_registration(registration_upload); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } fn start_client_login(&self, password: &str) -> Result { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_client_login(password); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } fn start_server_login( &self, @@ -125,7 +131,7 @@ impl OpaqueImpl for CipherConfiguration { username, ); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } fn finish_client_login( &self, @@ -136,7 +142,7 @@ impl OpaqueImpl for CipherConfiguration { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_client_login(state, credential_response, password); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } fn finish_server_login( &self, @@ -146,7 +152,7 @@ impl OpaqueImpl for CipherConfiguration { if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_server_login(state, credential_finalization); }; - Err(Error::InvalidInput("Invalid cipher configuration")) + Err(invalid_config(self)) } } @@ -164,6 +170,7 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { fn as_variant(config: &CipherConfiguration) -> Option { match config { CipherConfiguration { + opaque_version: 3, oprf_cs: OprfCs::Ristretto255, ke_group: KeGroup::Ristretto255, key_exchange: KeyExchange::TripleDh, @@ -176,13 +183,8 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { Ok(Argon2::new( argon2::Algorithm::Argon2id, argon2::Version::V0x13, - argon2::Params::new( - self.0.memory_kib, - self.0.iterations, - self.0.parallelism, - None, - ) - .map_err(|_| Error::InvalidInput("Invalid Argon2 parameters"))?, + argon2::Params::new(self.0.memory, self.0.iterations, self.0.parallelism, None) + .map_err(|_| Error::InvalidConfig("Invalid Argon2 parameters".into()))?, )) } } diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs index 616dcd71..4fe9d2e5 100644 --- a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs @@ -1,33 +1,48 @@ -#[derive(Debug, Clone, Copy)] +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use crate::Error; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum OprfCs { Ristretto255, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum KeGroup { Ristretto255, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum KeyExchange { + #[serde(alias = "tripleDH", alias = "triple-dh")] TripleDh, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", tag = "algorithm", content = "parameters")] pub enum Ksf { Argon2id(Argon2id), __NonExhaustive(()), } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Argon2id { - pub memory_kib: u32, + pub memory: u32, pub iterations: u32, pub parallelism: u32, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct CipherConfiguration { + pub opaque_version: u32, + #[serde(alias = "oprfCS")] pub oprf_cs: OprfCs, pub ke_group: KeGroup, pub key_exchange: KeyExchange, @@ -37,18 +52,27 @@ pub struct CipherConfiguration { impl Default for CipherConfiguration { fn default() -> Self { Self { + opaque_version: 3, oprf_cs: OprfCs::Ristretto255, ke_group: KeGroup::Ristretto255, key_exchange: KeyExchange::TripleDh, ksf: Ksf::Argon2id(Argon2id { - memory_kib: 64, - iterations: 3, - parallelism: 1, + memory: 65536, + iterations: 4, + parallelism: 4, }), } } } +impl FromStr for CipherConfiguration { + type Err = Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(|e| Error::InvalidConfig(e.to_string())) + } +} + pub(crate) struct ClientRegistrationStartResult { // The message is sent to the server for the next step of the registration protocol. pub(crate) registration_request: Vec, diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index 588e7b7b..ff460188 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -30,7 +30,12 @@ Bitwarden.OPAQUE bitwarden.png Bitwarden;OPAQUE;PAKE;.NET - 1.0.0 + + 0.5.0 + beta + 1 + + $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs index b6931a97..8958198e 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs @@ -9,7 +9,8 @@ private static string getCodeName(int code) { 0 => "OK", 1 => "INVALID_INPUT", - 2 => "PROTOCOL_ERROR", + 2 => "INVALID_CONFIG", + 3 => "PROTOCOL_ERROR", // This is a special case and it's only used in the C# code. 100 => "UNEXPECTED_RETURN", diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs index ee27cc9a..76e6b275 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -1,5 +1,7 @@ using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Bitwarden.OPAQUE; @@ -39,29 +41,29 @@ private struct Response private static partial void free_buffer(Buffer buf); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_client_registration(string password); + private static partial Response start_client_registration(string config, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_server_registration(Buffer server_setup, Buffer registration_request, string username); + private static partial Response start_server_registration(string config, Buffer server_setup, Buffer registration_request, string username); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_client_registration(Buffer state, Buffer registration_response, string password); + private static partial Response finish_client_registration(string config, Buffer state, Buffer registration_response, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_server_registration(Buffer registration_upload); + private static partial Response finish_server_registration(string config, Buffer registration_upload); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_client_login(string password); + private static partial Response start_client_login(string config, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_server_login(Buffer server_setup, Buffer server_registration, Buffer credential_request, string username); + private static partial Response start_server_login(string config, Buffer server_setup, Buffer server_registration, Buffer credential_request, string username); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_client_login(Buffer state, Buffer credential_response, string password); + private static partial Response finish_client_login(string config, Buffer state, Buffer credential_response, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_server_login(Buffer state, Buffer credential_finalization); + private static partial Response finish_server_login(string config, Buffer state, Buffer credential_finalization); private static Buffer BuildBuffer(byte[]? data, out GCHandle handle) { @@ -113,20 +115,29 @@ private static List HandleResponse(Response response, int expectedValues return arrays; } - internal static (byte[], byte[]) StartClientRegistration(string password) + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions { - var response = start_client_registration(password); + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, // Converts enums to strings + IncludeFields = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + internal static (byte[], byte[]) StartClientRegistration(CipherConfiguration config, string password) + { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var response = start_client_registration(configStr, password); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); } - internal static (byte[], byte[]) StartServerRegistration(byte[]? serverSetup, byte[] registrationRequest, string username) + internal static (byte[], byte[]) StartServerRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); var registrationRequestBuf = BuildBuffer(registrationRequest, out var registrationRequestHandle); try { - var response = start_server_registration(serverSetupBuf, registrationRequestBuf, username); + var response = start_server_registration(configStr, serverSetupBuf, registrationRequestBuf, username); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); } @@ -135,17 +146,17 @@ internal static (byte[], byte[]) StartServerRegistration(byte[]? serverSetup, by serverSetupHandle.Free(); registrationRequestHandle.Free(); } - } - internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] state, byte[] registrationResponse, string password) + internal static (byte[], byte[], byte[]) FinishClientRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); var stateBuf = BuildBuffer(state, out var stateHandle); var registrationResponseBuf = BuildBuffer(registrationResponse, out var registrationResponseHandle); try { - var response = finish_client_registration(stateBuf, registrationResponseBuf, password); + var response = finish_client_registration(configStr, stateBuf, registrationResponseBuf, password); var ret = HandleResponse(response, 3); return (ret[0], ret[1], ret[2]); } @@ -156,36 +167,38 @@ internal static (byte[], byte[], byte[]) FinishClientRegistration(byte[] state, } } - internal static byte[] FinishServerRegistration(byte[] registrationUpload) + internal static byte[] FinishServerRegistration(CipherConfiguration config, byte[] registrationUpload) { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); var registrationUploadBuf = BuildBuffer(registrationUpload, out var handle); try { - var response = finish_server_registration(registrationUploadBuf); + var response = finish_server_registration(configStr, registrationUploadBuf); return HandleResponse(response, 1)[0]; } finally { handle.Free(); } - } - internal static (byte[], byte[]) StartClientLogin(string password) + internal static (byte[], byte[]) StartClientLogin(CipherConfiguration config, string password) { - var response = start_client_login(password); + var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var response = start_client_login(configStr, password); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); } - internal static (byte[], byte[]) StartServerLogin(byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) + internal static (byte[], byte[]) StartServerLogin(CipherConfiguration config, byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); var serverRegistrationBuf = BuildBuffer(serverRegistration, out var serverRegistrationHandle); var credentialRequestBuf = BuildBuffer(credentialRequest, out var credentialRequestHandle); try { - var response = start_server_login(serverSetupBuf, serverRegistrationBuf, credentialRequestBuf, username); + var response = start_server_login(configStr, serverSetupBuf, serverRegistrationBuf, credentialRequestBuf, username); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); } @@ -198,14 +211,15 @@ internal static (byte[], byte[]) StartServerLogin(byte[] serverSetup, byte[] ser } - internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(byte[] state, byte[] credentialResponse, string password) + internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(CipherConfiguration config, byte[] state, byte[] credentialResponse, string password) { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); var stateBuf = BuildBuffer(state, out var stateHandle); var credentialResponseBuf = BuildBuffer(credentialResponse, out var credentialResponseHandle); try { - var response = finish_client_login(stateBuf, credentialResponseBuf, password); + var response = finish_client_login(configStr, stateBuf, credentialResponseBuf, password); var ret = HandleResponse(response, 4); return (ret[0], ret[1], ret[2], ret[3]); } @@ -216,13 +230,14 @@ internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(byte[] state, } } - internal static byte[] FinishServerLogin(byte[] state, byte[] credentialFinalization) + internal static byte[] FinishServerLogin(CipherConfiguration config, byte[] state, byte[] credentialFinalization) { + var configStr = JsonSerializer.Serialize(config, SerializerOptions); var stateBuf = BuildBuffer(state, out var stateHandle); var credentialFinalizationBuf = BuildBuffer(credentialFinalization, out var credentialFinalizationHandle); try { - var response = finish_server_login(stateBuf, credentialFinalizationBuf); + var response = finish_server_login(configStr, stateBuf, credentialFinalizationBuf); return HandleResponse(response, 1)[0]; } finally diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs index 52d9daec..93f4bb24 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs @@ -48,7 +48,7 @@ public sealed partial class BitwardenOpaqueClient /// public ClientRegistrationStartResult StartRegistration(CipherConfiguration config, string password) { - var (registrationRequest, state) = BitwardenLibrary.StartClientRegistration(password); + var (registrationRequest, state) = BitwardenLibrary.StartClientRegistration(config, password); return new ClientRegistrationStartResult { registrationRequest = registrationRequest, @@ -66,7 +66,7 @@ public ClientRegistrationStartResult StartRegistration(CipherConfiguration confi /// public ClientRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) { - var (registrationUpload, exportKey, serverSPKey) = BitwardenLibrary.FinishClientRegistration(state, registrationResponse, password); + var (registrationUpload, exportKey, serverSPKey) = BitwardenLibrary.FinishClientRegistration(config, state, registrationResponse, password); return new ClientRegistrationFinishResult { registrationUpload = registrationUpload, @@ -78,7 +78,7 @@ public ClientRegistrationFinishResult FinishRegistration(CipherConfiguration con public ClientLoginStartResult StartLogin(CipherConfiguration config, string password) { - var (credentialRequest, state) = BitwardenLibrary.StartClientLogin(password); + var (credentialRequest, state) = BitwardenLibrary.StartClientLogin(config, password); return new ClientLoginStartResult { credentialRequest = credentialRequest, @@ -89,7 +89,7 @@ public ClientLoginStartResult StartLogin(CipherConfiguration config, string pass public ClientLoginFinishResult FinishLogin(CipherConfiguration config, byte[] state, byte[] credentialResponse, string password) { - var (credentialFinalization, sessionKey, exportKey, serverSPKey) = BitwardenLibrary.FinishClientLogin(state, credentialResponse, password); + var (credentialFinalization, sessionKey, exportKey, serverSPKey) = BitwardenLibrary.FinishClientLogin(config, state, credentialResponse, password); return new ClientLoginFinishResult { credentialFinalization = credentialFinalization, diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs index 2e21d9dc..eb36c071 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs @@ -44,7 +44,7 @@ public sealed partial class BitwardenOpaqueServer /// public ServerRegistrationStartResult StartRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) { - var (registrationResponse, serverSetupRet) = BitwardenLibrary.StartServerRegistration(serverSetup, registrationRequest, username); + var (registrationResponse, serverSetupRet) = BitwardenLibrary.StartServerRegistration(config, serverSetup, registrationRequest, username); return new ServerRegistrationStartResult { registrationResponse = registrationResponse, @@ -60,7 +60,7 @@ public ServerRegistrationStartResult StartRegistration(CipherConfiguration confi /// public ServerRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] registrationUpload) { - var serverRegistration = BitwardenLibrary.FinishServerRegistration(registrationUpload); + var serverRegistration = BitwardenLibrary.FinishServerRegistration(config, registrationUpload); return new ServerRegistrationFinishResult { serverRegistration = serverRegistration @@ -69,7 +69,7 @@ public ServerRegistrationFinishResult FinishRegistration(CipherConfiguration con public ServerLoginStartResult StartLogin(CipherConfiguration config, byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) { - var (credentialResponse, state) = BitwardenLibrary.StartServerLogin(serverSetup, serverRegistration, credentialRequest, username); + var (credentialResponse, state) = BitwardenLibrary.StartServerLogin(config, serverSetup, serverRegistration, credentialRequest, username); return new ServerLoginStartResult { credentialResponse = credentialResponse, @@ -79,7 +79,7 @@ public ServerLoginStartResult StartLogin(CipherConfiguration config, byte[] serv public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] state, byte[] credentialFinalization) { - var sessionKey = BitwardenLibrary.FinishServerLogin(state, credentialFinalization); + var sessionKey = BitwardenLibrary.FinishServerLogin(config, state, credentialFinalization); return new ServerLoginFinishResult { sessionKey = sessionKey diff --git a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs index ccf44878..46378e33 100644 --- a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs +++ b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs @@ -21,20 +21,38 @@ public enum KeyExchange TripleDH } -/// A key stretching function, typically used for password hashing -public abstract record KSF; +/// A key stretching algorithm +public enum KsfAlgorithm +{ + /// The Argon2id key stretching function + Argon2id +} -/// -/// Argon2id key stretching function -/// -/// Iteration count -/// Memory in KibiBytes -/// Parallelism count -public record Argon2id(int iterations, int memoryKiB, int parallelism) : KSF; +/// Key stretching function parameters +public struct KsfParameters +{ + /// The number of iterations to use + public int Iterations; + /// The amount of memory to use in KiB + public int Memory; + /// The number of threads to use + public int Parallelism; +} + +/// A key stretching function, typically used for password hashing +public struct Ksf +{ + /// The key stretching function to use + public KsfAlgorithm Algorithm; + /// The parameters for the key stretching function + public KsfParameters Parameters; +} /// Configures the underlying primitives used in OPAQUE public struct CipherConfiguration { + /// The version of the OPAQUE-ke protocol to use + public int OpaqueVersion; /// A VOPRF ciphersuite public OprfCS OprfCS; /// A `Group` used for the `KeyExchange`. @@ -42,14 +60,24 @@ public struct CipherConfiguration /// The key exchange protocol to use in the login step public KeyExchange KeyExchange; /// A key stretching function, typically used for password hashing - public KSF KSF; + public Ksf Ksf; /// The default configuration for the OPAQUE protocol public static readonly CipherConfiguration Default = new CipherConfiguration { + OpaqueVersion = 3, OprfCS = OprfCS.Ristretto255, KeGroup = KeGroup.Ristretto255, KeyExchange = KeyExchange.TripleDH, - KSF = new Argon2id(4, 65536, 4) + Ksf = new Ksf + { + Algorithm = KsfAlgorithm.Argon2id, + Parameters = new KsfParameters + { + Iterations = 4, + Memory = 65536, + Parallelism = 4 + } + } }; } diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index e6fa9e07..913bf9d0 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -15,7 +15,24 @@ public void TestRegistration() var server = new BitwardenOpaqueServer(); var client = new BitwardenOpaqueClient(); - var config = CipherConfiguration.Default; + // Lower the config values so the test runs fast + var config = new CipherConfiguration + { + OpaqueVersion = 3, + OprfCS = OprfCS.Ristretto255, + KeGroup = KeGroup.Ristretto255, + KeyExchange = KeyExchange.TripleDH, + Ksf = new Ksf + { + Algorithm = KsfAlgorithm.Argon2id, + Parameters = new KsfParameters + { + Iterations = 1, + Memory = 1024, + Parallelism = 1 + } + } + }; ///// Registration From f6a24e3e1afe02cd96af05fc4e81784a2dba6d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 13 Mar 2025 17:08:36 +0100 Subject: [PATCH 52/89] Fix some naming and serializing --- .../src/CipherConfiguration.cs | 61 ++++++++++++++----- extensions/Bitwarden.OPAQUE/tests/Tests.cs | 2 +- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs index 46378e33..58ecdcb8 100644 --- a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs +++ b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs @@ -1,13 +1,40 @@ -namespace Bitwarden.OPAQUE; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Bitwarden.OPAQUE; + +class KeyExchangeConverter : JsonConverter +{ + public override KeyExchange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString() ?? throw new JsonException("Invalid value"); + return value.ToLower() switch + { + "triple-dh" => KeyExchange.TripleDH, + "tripledh" => KeyExchange.TripleDH, // Handles both formats + _ => throw new JsonException($"Unknown value: {value}") + }; + } + + public override void Write(Utf8JsonWriter writer, KeyExchange value, JsonSerializerOptions options) + { + writer.WriteStringValue(value switch + { + KeyExchange.TripleDH => "triple-dh", + }); + } +} /// A VOPRF ciphersuite -public enum OprfCS +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum OprfCs { /// The Ristretto255 ciphersuite Ristretto255 = 0, } /// A `Group` used for the `KeyExchange`. +[JsonConverter(typeof(JsonStringEnumConverter))] public enum KeGroup { /// The Ristretto255 group @@ -15,6 +42,7 @@ public enum KeGroup } /// The key exchange protocol to use in the login step +[JsonConverter(typeof(KeyExchangeConverter))] public enum KeyExchange { /// The Triple Diffie-Hellman key exchange implementation @@ -22,6 +50,7 @@ public enum KeyExchange } /// A key stretching algorithm +[JsonConverter(typeof(JsonStringEnumConverter))] public enum KsfAlgorithm { /// The Argon2id key stretching function @@ -29,44 +58,44 @@ public enum KsfAlgorithm } /// Key stretching function parameters -public struct KsfParameters +public class KsfParameters { /// The number of iterations to use - public int Iterations; + public int Iterations { get; set; } /// The amount of memory to use in KiB - public int Memory; + public int Memory { get; set; } /// The number of threads to use - public int Parallelism; + public int Parallelism { get; set; } } /// A key stretching function, typically used for password hashing -public struct Ksf +public class Ksf { /// The key stretching function to use - public KsfAlgorithm Algorithm; + public KsfAlgorithm Algorithm { get; set; } /// The parameters for the key stretching function - public KsfParameters Parameters; + public required KsfParameters Parameters { get; set; } } /// Configures the underlying primitives used in OPAQUE -public struct CipherConfiguration +public class CipherConfiguration { /// The version of the OPAQUE-ke protocol to use - public int OpaqueVersion; + public int OpaqueVersion { get; set; } /// A VOPRF ciphersuite - public OprfCS OprfCS; + public OprfCs OprfCs { get; set; } /// A `Group` used for the `KeyExchange`. - public KeGroup KeGroup; + public KeGroup KeGroup { get; set; } /// The key exchange protocol to use in the login step - public KeyExchange KeyExchange; + public KeyExchange KeyExchange { get; set; } /// A key stretching function, typically used for password hashing - public Ksf Ksf; + public required Ksf Ksf { get; set; } /// The default configuration for the OPAQUE protocol public static readonly CipherConfiguration Default = new CipherConfiguration { OpaqueVersion = 3, - OprfCS = OprfCS.Ristretto255, + OprfCs = OprfCs.Ristretto255, KeGroup = KeGroup.Ristretto255, KeyExchange = KeyExchange.TripleDH, Ksf = new Ksf diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index 913bf9d0..5fbcb85b 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -19,7 +19,7 @@ public void TestRegistration() var config = new CipherConfiguration { OpaqueVersion = 3, - OprfCS = OprfCS.Ristretto255, + OprfCs = OprfCs.Ristretto255, KeGroup = KeGroup.Ristretto255, KeyExchange = KeyExchange.TripleDH, Ksf = new Ksf From e4d674bd9ecbeb5e9023780a345e2b4f99545b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 17 Mar 2025 18:07:42 +0100 Subject: [PATCH 53/89] Fix lint --- .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs index 76e6b275..5a86ed80 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -115,16 +115,15 @@ private static List HandleResponse(Response response, int expectedValues return arrays; } - private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + private static readonly JsonSerializerOptions _serializerOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, // Converts enums to strings - IncludeFields = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; internal static (byte[], byte[]) StartClientRegistration(CipherConfiguration config, string password) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var response = start_client_registration(configStr, password); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); @@ -132,7 +131,7 @@ internal static (byte[], byte[]) StartClientRegistration(CipherConfiguration con internal static (byte[], byte[]) StartServerRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); var registrationRequestBuf = BuildBuffer(registrationRequest, out var registrationRequestHandle); try @@ -150,7 +149,7 @@ internal static (byte[], byte[]) StartServerRegistration(CipherConfiguration con internal static (byte[], byte[], byte[]) FinishClientRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var stateBuf = BuildBuffer(state, out var stateHandle); var registrationResponseBuf = BuildBuffer(registrationResponse, out var registrationResponseHandle); @@ -169,7 +168,7 @@ internal static (byte[], byte[], byte[]) FinishClientRegistration(CipherConfigur internal static byte[] FinishServerRegistration(CipherConfiguration config, byte[] registrationUpload) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var registrationUploadBuf = BuildBuffer(registrationUpload, out var handle); try { @@ -184,7 +183,7 @@ internal static byte[] FinishServerRegistration(CipherConfiguration config, byte internal static (byte[], byte[]) StartClientLogin(CipherConfiguration config, string password) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var response = start_client_login(configStr, password); var ret = HandleResponse(response, 2); return (ret[0], ret[1]); @@ -192,7 +191,7 @@ internal static (byte[], byte[]) StartClientLogin(CipherConfiguration config, st internal static (byte[], byte[]) StartServerLogin(CipherConfiguration config, byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); var serverRegistrationBuf = BuildBuffer(serverRegistration, out var serverRegistrationHandle); var credentialRequestBuf = BuildBuffer(credentialRequest, out var credentialRequestHandle); @@ -213,7 +212,7 @@ internal static (byte[], byte[]) StartServerLogin(CipherConfiguration config, by internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(CipherConfiguration config, byte[] state, byte[] credentialResponse, string password) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var stateBuf = BuildBuffer(state, out var stateHandle); var credentialResponseBuf = BuildBuffer(credentialResponse, out var credentialResponseHandle); @@ -232,7 +231,7 @@ internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(CipherConfigu internal static byte[] FinishServerLogin(CipherConfiguration config, byte[] state, byte[] credentialFinalization) { - var configStr = JsonSerializer.Serialize(config, SerializerOptions); + var configStr = JsonSerializer.Serialize(config, _serializerOptions); var stateBuf = BuildBuffer(state, out var stateHandle); var credentialFinalizationBuf = BuildBuffer(credentialFinalization, out var credentialFinalizationHandle); try From 8f57dd5ae4c915de520fcae85f13f358364f5cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 17 Mar 2025 18:29:11 +0100 Subject: [PATCH 54/89] Add session key equal test --- extensions/Bitwarden.OPAQUE/tests/Tests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index 5fbcb85b..cf4e1a17 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -66,5 +66,7 @@ public void TestRegistration() Assert.NotNull(serverLoginFinishResult.sessionKey); + Assert.Equal(serverLoginFinishResult.sessionKey, clientLoginFinishResult.sessionKey); + } } From e0ee86bf9b694691b7884063d7b95e23da414e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 17 Mar 2025 18:31:41 +0100 Subject: [PATCH 55/89] OPAQUE -> Opaque --- .../workflows/build-rust-cross-platform.yml | 10 +++++----- .github/workflows/build.yml | 6 +++--- .vscode/settings.json | 2 +- bitwarden-dotnet.sln | 6 +++--- .../src/Bitwarden.OPAQUE.csproj | 10 +++++----- .../Bitwarden.OPAQUE/src/BitwardenException.cs | 2 +- .../Bitwarden.OPAQUE/src/BitwardenLibrary.cs | 2 +- .../src/BitwardenOpaqueClient.cs | 18 +++++++++--------- .../src/BitwardenOpaqueServer.cs | 16 ++++++++-------- .../src/CipherConfiguration.cs | 2 +- .../tests/Bitwarden.OPAQUE.Tests.csproj | 2 +- extensions/Bitwarden.OPAQUE/tests/Tests.cs | 2 +- 12 files changed, 39 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 1d1d1410..b82f438f 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -65,7 +65,7 @@ jobs: RUSTFLAGS: "-D warnings" MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems run: cargo build --target ${{ matrix.settings.target }} --release - working-directory: extensions/Bitwarden.OPAQUE/rust + working-directory: extensions/Bitwarden.Opaque/rust # Build Rust using cross - name: Build Rust cross for - ${{ matrix.settings.target }} @@ -73,7 +73,7 @@ jobs: env: RUSTFLAGS: "-D warnings" run: cross build --target ${{ matrix.settings.target }} --release - working-directory: extensions/Bitwarden.OPAQUE/rust + working-directory: extensions/Bitwarden.Opaque/rust - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 @@ -83,9 +83,9 @@ jobs: retention-days: 1 if-no-files-found: error path: | - extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll - extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so - extensions/Bitwarden.OPAQUE/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.dylib + extensions/Bitwarden.Opaque/rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll + extensions/Bitwarden.Opaque/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so + extensions/Bitwarden.Opaque/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.dylib collect_artifacts: name: Collect and Upload All Artifacts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e420aaef..1e0b878b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: libopaque_ke_binding_all_files - path: extensions/Bitwarden.OPAQUE/rust/dist/ + path: extensions/Bitwarden.Opaque/rust/dist/ - name: Pack solution run: dotnet pack -c Release --output ./nuget-output @@ -47,7 +47,7 @@ jobs: - name: Upload NuGet package uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: - name: Bitwarden.OPAQUE.nupkg + name: Bitwarden.Opaque.nupkg if-no-files-found: error path: | - ./nuget-output/Bitwarden.OPAQUE*.nupkg + ./nuget-output/Bitwarden.Opaque*.nupkg diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d5485af..b7ef8653 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.linkedProjects": ["extensions/Bitwarden.OPAQUE/rust/Cargo.toml"], + "rust-analyzer.linkedProjects": ["extensions/Bitwarden.Opaque/rust/Cargo.toml"], } diff --git a/bitwarden-dotnet.sln b/bitwarden-dotnet.sln index d8663c91..412513eb 100644 --- a/bitwarden-dotnet.sln +++ b/bitwarden-dotnet.sln @@ -45,11 +45,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4949B721 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Server.Sdk.Features.Tests", "extensions\Bitwarden.Server.Sdk.Features\tests\Bitwarden.Server.Sdk.Features.Tests\Bitwarden.Server.Sdk.Features.Tests.csproj", "{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bitwarden.OPAQUE", "Bitwarden.OPAQUE", "{023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bitwarden.Opaque", "Bitwarden.Opaque", "{023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.OPAQUE", "extensions\Bitwarden.OPAQUE\src\Bitwarden.OPAQUE.csproj", "{B49E33DF-A672-4361-BECA-C3DA423BD7A9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque", "extensions\Bitwarden.Opaque\src\Bitwarden.Opaque.csproj", "{B49E33DF-A672-4361-BECA-C3DA423BD7A9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.OPAQUE.Tests", "extensions\Bitwarden.OPAQUE\tests\Bitwarden.OPAQUE.Tests.csproj", "{DC9DAB81-5ED7-4756-BF20-D594D87D2865}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque.Tests", "extensions\Bitwarden.Opaque\tests\Bitwarden.Opaque.Tests.csproj", "{DC9DAB81-5ED7-4756-BF20-D594D87D2865}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj index ff460188..1a6055c9 100644 --- a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj +++ b/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj @@ -22,16 +22,16 @@ true true - Bitwarden.OPAQUE + Bitwarden.Opaque Bitwarden OPAQUE-KE library Bitwarden Inc. - OPAQUE + Opaque - Bitwarden.OPAQUE + Bitwarden.Opaque bitwarden.png - Bitwarden;OPAQUE;PAKE;.NET + Bitwarden;Opaque;PAKE;.NET - 0.5.0 + 0.0.1 beta 1 diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs index 8958198e..33d5e067 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs @@ -1,4 +1,4 @@ -namespace Bitwarden.OPAQUE; +namespace Bitwarden.Opaque; public class BitwardenException(int errorCode, string message) : Exception($"Error {getCodeName(errorCode)} - {message}") { diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs index 5a86ed80..279f0843 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Bitwarden.OPAQUE; +namespace Bitwarden.Opaque; internal static partial class BitwardenLibrary { diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs index 93f4bb24..b1371971 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs @@ -1,20 +1,20 @@ -namespace Bitwarden.OPAQUE; +namespace Bitwarden.Opaque; #pragma warning disable CA1822 // Mark members as static -/// The result of +/// The result of public struct ClientRegistrationStartResult { - /// The registration response which is then passed to . + /// The registration response which is then passed to . public byte[] registrationRequest; - /// The client state, which must be kept on the client for . + /// The client state, which must be kept on the client for . public byte[] state; } -/// The result of +/// The result of public struct ClientRegistrationFinishResult { - /// The registration upload which is then passed to . + /// The registration upload which is then passed to . public byte[] registrationUpload; /// The export key output by client registration public byte[] exportKey; @@ -57,11 +57,11 @@ public ClientRegistrationStartResult StartRegistration(CipherConfiguration confi } /// - /// Finish the server registration process. This must happen after + /// Finish the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation - /// The state obtained from the client start operation, - /// The server registration response, + /// The state obtained from the client start operation, + /// The server registration response, /// The password to register /// public ClientRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs index eb36c071..16dc0101 100644 --- a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs @@ -1,17 +1,17 @@ -namespace Bitwarden.OPAQUE; +namespace Bitwarden.Opaque; #pragma warning disable CA1822 // Mark members as static -/// The result of +/// The result of public struct ServerRegistrationStartResult { - /// The registration response which is then passed to . + /// The registration response which is then passed to . public byte[] registrationResponse; /// The server setup, which needs to be persisted on the server for future logins. public byte[] serverSetup; } -/// The result of +/// The result of public struct ServerRegistrationFinishResult { @@ -35,11 +35,11 @@ public struct ServerLoginFinishResult public sealed partial class BitwardenOpaqueServer { /// - /// Start the server registration process. This must happen after + /// Start the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation /// The server setup. Use null to let the library create a new random one - /// The client registration request, + /// The client registration request, /// The username to register /// public ServerRegistrationStartResult StartRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) @@ -53,10 +53,10 @@ public ServerRegistrationStartResult StartRegistration(CipherConfiguration confi } /// - /// Finish the server registration process. This must happen after + /// Finish the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation - /// The client registration upload, + /// The client registration upload, /// public ServerRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] registrationUpload) { diff --git a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs index 58ecdcb8..7411e05d 100644 --- a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs +++ b/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Bitwarden.OPAQUE; +namespace Bitwarden.Opaque; class KeyExchangeConverter : JsonConverter { diff --git a/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj b/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj index d9de6845..182d1a39 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj +++ b/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.OPAQUE/tests/Tests.cs index cf4e1a17..55817728 100644 --- a/extensions/Bitwarden.OPAQUE/tests/Tests.cs +++ b/extensions/Bitwarden.OPAQUE/tests/Tests.cs @@ -1,4 +1,4 @@ -namespace Bitwarden.OPAQUE.Tests; +namespace Bitwarden.Opaque.Tests; using Xunit; From ecdf967fd5c206963b84b0deb090db1d3a1b66b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 17 Mar 2025 18:33:29 +0100 Subject: [PATCH 56/89] Update release workflow --- .github/workflows/start-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/start-release.yml b/.github/workflows/start-release.yml index e9df2f28..4221847e 100644 --- a/.github/workflows/start-release.yml +++ b/.github/workflows/start-release.yml @@ -11,6 +11,7 @@ on: options: - Bitwarden.Server.Sdk - Bitwarden.Server.Sdk.Features + - Bitwarden.Opaque permissions: pull-requests: write From 66687c35dd93f6c404226f7695fe64457e2e6d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 17 Mar 2025 18:38:17 +0100 Subject: [PATCH 57/89] Rename files --- .../rust/.gitignore | 0 .../rust/Cargo.lock | 0 .../rust/Cargo.toml | 0 .../rust/src/ffi/mod.rs | 0 .../rust/src/ffi/types.rs | 0 .../rust/src/lib.rs | 0 .../rust/src/opaque/mod.rs | 0 .../rust/src/opaque/types.rs | 0 .../src/Bitwarden.Opaque.csproj} | 0 .../src/BitwardenException.cs | 0 .../src/BitwardenLibrary.cs | 0 .../src/BitwardenOpaqueClient.cs | 0 .../src/BitwardenOpaqueServer.cs | 0 .../src/CipherConfiguration.cs | 0 .../src/README.md | 0 .../src/bitwarden.png | Bin .../tests/Bitwarden.Opaque.Tests.csproj} | 0 .../tests/Tests.cs | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/.gitignore (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/Cargo.lock (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/Cargo.toml (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/src/ffi/mod.rs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/src/ffi/types.rs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/src/lib.rs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/src/opaque/mod.rs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/rust/src/opaque/types.rs (100%) rename extensions/{Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj => Bitwarden.Opaque/src/Bitwarden.Opaque.csproj} (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/BitwardenException.cs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/BitwardenLibrary.cs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/BitwardenOpaqueClient.cs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/BitwardenOpaqueServer.cs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/CipherConfiguration.cs (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/README.md (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/src/bitwarden.png (100%) rename extensions/{Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj => Bitwarden.Opaque/tests/Bitwarden.Opaque.Tests.csproj} (100%) rename extensions/{Bitwarden.OPAQUE => Bitwarden.Opaque}/tests/Tests.cs (100%) diff --git a/extensions/Bitwarden.OPAQUE/rust/.gitignore b/extensions/Bitwarden.Opaque/rust/.gitignore similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/.gitignore rename to extensions/Bitwarden.Opaque/rust/.gitignore diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.lock b/extensions/Bitwarden.Opaque/rust/Cargo.lock similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/Cargo.lock rename to extensions/Bitwarden.Opaque/rust/Cargo.lock diff --git a/extensions/Bitwarden.OPAQUE/rust/Cargo.toml b/extensions/Bitwarden.Opaque/rust/Cargo.toml similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/Cargo.toml rename to extensions/Bitwarden.Opaque/rust/Cargo.toml diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/src/ffi/mod.rs rename to extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs diff --git a/extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/src/ffi/types.rs rename to extensions/Bitwarden.Opaque/rust/src/ffi/types.rs diff --git a/extensions/Bitwarden.OPAQUE/rust/src/lib.rs b/extensions/Bitwarden.Opaque/rust/src/lib.rs similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/src/lib.rs rename to extensions/Bitwarden.Opaque/rust/src/lib.rs diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/src/opaque/mod.rs rename to extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs diff --git a/extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs similarity index 100% rename from extensions/Bitwarden.OPAQUE/rust/src/opaque/types.rs rename to extensions/Bitwarden.Opaque/rust/src/opaque/types.rs diff --git a/extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/Bitwarden.OPAQUE.csproj rename to extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenException.cs b/extensions/Bitwarden.Opaque/src/BitwardenException.cs similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/BitwardenException.cs rename to extensions/Bitwarden.Opaque/src/BitwardenException.cs diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/BitwardenLibrary.cs rename to extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueClient.cs rename to extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs diff --git a/extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/BitwardenOpaqueServer.cs rename to extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs diff --git a/extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs b/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/CipherConfiguration.cs rename to extensions/Bitwarden.Opaque/src/CipherConfiguration.cs diff --git a/extensions/Bitwarden.OPAQUE/src/README.md b/extensions/Bitwarden.Opaque/src/README.md similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/README.md rename to extensions/Bitwarden.Opaque/src/README.md diff --git a/extensions/Bitwarden.OPAQUE/src/bitwarden.png b/extensions/Bitwarden.Opaque/src/bitwarden.png similarity index 100% rename from extensions/Bitwarden.OPAQUE/src/bitwarden.png rename to extensions/Bitwarden.Opaque/src/bitwarden.png diff --git a/extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj b/extensions/Bitwarden.Opaque/tests/Bitwarden.Opaque.Tests.csproj similarity index 100% rename from extensions/Bitwarden.OPAQUE/tests/Bitwarden.OPAQUE.Tests.csproj rename to extensions/Bitwarden.Opaque/tests/Bitwarden.Opaque.Tests.csproj diff --git a/extensions/Bitwarden.OPAQUE/tests/Tests.cs b/extensions/Bitwarden.Opaque/tests/Tests.cs similarity index 100% rename from extensions/Bitwarden.OPAQUE/tests/Tests.cs rename to extensions/Bitwarden.Opaque/tests/Tests.cs From 3f14fb26fdad423ee12e3efb832cc4d41f925abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 17 Mar 2025 19:00:55 +0100 Subject: [PATCH 58/89] Fix suffix --- extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj index 1a6055c9..0a7c6f92 100644 --- a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj +++ b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj @@ -34,8 +34,7 @@ 0.0.1 beta 1 - - $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) + $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) From e7414dbd413fa832292903095a211dd539b6cce2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 17 Mar 2025 18:01:55 +0000 Subject: [PATCH 59/89] Bump Bitwarden.Opaque version to 0.0.1-beta.2 --- extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj index 0a7c6f92..2824de79 100644 --- a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj +++ b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj @@ -33,7 +33,7 @@ 0.0.1 beta - 1 + 2 $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) @@ -59,13 +59,11 @@ libopaque_ke_binding.dylib - + - + Always From b34c5b05ea44e97869ad1463cba9918c6e90aefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 18 Mar 2025 18:39:43 +0100 Subject: [PATCH 60/89] Improve how the type conversions are done and add missing docs --- .../src/BitwardenException.cs | 5 + .../Bitwarden.Opaque/src/BitwardenLibrary.cs | 242 ++++++------------ .../src/BitwardenOpaqueClient.cs | 81 ++++-- .../src/BitwardenOpaqueServer.cs | 69 +++-- 4 files changed, 186 insertions(+), 211 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/BitwardenException.cs b/extensions/Bitwarden.Opaque/src/BitwardenException.cs index 33d5e067..3ddf0292 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenException.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenException.cs @@ -1,5 +1,10 @@ namespace Bitwarden.Opaque; +/// +/// A class to represent an exception thrown by the Bitwarden OPAQUE library. +/// +/// A numeric error code, to separate different error types +/// The error message public class BitwardenException(int errorCode, string message) : Exception($"Error {getCodeName(errorCode)} - {message}") { private static string getCodeName(int code) diff --git a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs index 279f0843..6f02b19d 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs @@ -14,7 +14,7 @@ internal static partial class BitwardenLibrary /// of the Buffer type in the rust crate, both in field type and order. /// [StructLayout(LayoutKind.Sequential)] - private struct Buffer + internal struct Buffer { public IntPtr data; public nint size; @@ -26,7 +26,7 @@ private struct Buffer /// of the Response type in the rust crate, both in field type and order. /// [StructLayout(LayoutKind.Sequential)] - private struct Response + internal struct Response { public nint error; public Buffer error_message; @@ -35,44 +35,70 @@ private struct Response public Buffer data2; public Buffer data3; public Buffer data4; + + // Utility function to get all buffers as a list + public List GetAllBuffers() + { + return new List { data1, data2, data3, data4 }; + } } [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial void free_buffer(Buffer buf); + internal static partial void free_buffer(Buffer buf); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_client_registration(string config, string password); + internal static partial Response start_client_registration(string config, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_server_registration(string config, Buffer server_setup, Buffer registration_request, string username); + internal static partial Response start_server_registration(string config, Buffer server_setup, Buffer registration_request, string username); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_client_registration(string config, Buffer state, Buffer registration_response, string password); + internal static partial Response finish_client_registration(string config, Buffer state, Buffer registration_response, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_server_registration(string config, Buffer registration_upload); + internal static partial Response finish_server_registration(string config, Buffer registration_upload); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_client_login(string config, string password); + internal static partial Response start_client_login(string config, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response start_server_login(string config, Buffer server_setup, Buffer server_registration, Buffer credential_request, string username); + internal static partial Response start_server_login(string config, Buffer server_setup, Buffer server_registration, Buffer credential_request, string username); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_client_login(string config, Buffer state, Buffer credential_response, string password); + internal static partial Response finish_client_login(string config, Buffer state, Buffer credential_response, string password); [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] - private static partial Response finish_server_login(string config, Buffer state, Buffer credential_finalization); + internal static partial Response finish_server_login(string config, Buffer state, Buffer credential_finalization); - private static Buffer BuildBuffer(byte[]? data, out GCHandle handle) + internal class FFIHandler { - handle = GCHandle.Alloc(data, GCHandleType.Pinned); - return new Buffer + private static readonly JsonSerializerOptions _serializerOptions = new() { - data = handle.AddrOfPinnedObject(), - size = data?.Length ?? 0 + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, // Converts enums to strings + PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private List _handles = new List(); + + internal void FreeHandles() + { + foreach (var handle in _handles) handle.Free(); + _handles.Clear(); + } + public Buffer Buf(byte[]? data) + { + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + _handles.Add(handle); + return new Buffer + { + data = handle.AddrOfPinnedObject(), + size = data?.Length ?? 0 + }; + } + public string Cfg(CipherConfiguration config) + { + return JsonSerializer.Serialize(config, _serializerOptions); + } } private static byte[]? CopyAndFreeBuffer(Buffer buffer) @@ -86,165 +112,43 @@ private static Buffer BuildBuffer(byte[]? data, out GCHandle handle) return data; } - private static List HandleResponse(Response response, int expectedValues) - { - // If we receive an error, parse the message and throw an exception - if (response.error != 0) - { - var message = CopyAndFreeBuffer(response.error_message); - string messageStr; - try { messageStr = Encoding.UTF8.GetString(message!); } catch { messageStr = ""; } - throw new BitwardenException((int)response.error, messageStr); - } - - // If we don't receive an error, parse all the return types - var buffers = new Buffer[] { response.data1, response.data2, response.data3, response.data4 }; - var arrays = new List { }; - - foreach (var buffer in buffers) - { - var data = CopyAndFreeBuffer(buffer); - if (data == null) break; - arrays.Add(data); - } - if (arrays.Count != expectedValues) - { - throw new BitwardenException(100, $"Invalid number of return values. Expected {expectedValues}, got {arrays.Count}"); - } - - return arrays; - } - - private static readonly JsonSerializerOptions _serializerOptions = new() - { - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, // Converts enums to strings - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - internal static (byte[], byte[]) StartClientRegistration(CipherConfiguration config, string password) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var response = start_client_registration(configStr, password); - var ret = HandleResponse(response, 2); - return (ret[0], ret[1]); - } - - internal static (byte[], byte[]) StartServerRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); - var registrationRequestBuf = BuildBuffer(registrationRequest, out var registrationRequestHandle); - try - { - var response = start_server_registration(configStr, serverSetupBuf, registrationRequestBuf, username); - var ret = HandleResponse(response, 2); - return (ret[0], ret[1]); - } - finally - { - serverSetupHandle.Free(); - registrationRequestHandle.Free(); - } - } - - internal static (byte[], byte[], byte[]) FinishClientRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) + internal static List ExecuteFFIFunction(Func function, int expectedValues) { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var stateBuf = BuildBuffer(state, out var stateHandle); - var registrationResponseBuf = BuildBuffer(registrationResponse, out var registrationResponseHandle); - + var ffi = new FFIHandler(); try { - var response = finish_client_registration(configStr, stateBuf, registrationResponseBuf, password); - var ret = HandleResponse(response, 3); - return (ret[0], ret[1], ret[2]); + // Execute the function and get the response + var response = function(ffi); + + // If we receive an error, parse the message and throw an exception + if (response.error != 0) + { + var message = CopyAndFreeBuffer(response.error_message); + string messageStr; + try { messageStr = Encoding.UTF8.GetString(message!); } catch { messageStr = ""; } + throw new BitwardenException((int)response.error, messageStr); + } + + // If we don't receive an error, parse all the return types + var arrays = new List { }; + foreach (var buffer in response.GetAllBuffers()) + { + var data = CopyAndFreeBuffer(buffer); + if (data == null) break; + arrays.Add(data); + } + + // If we receive a different number of return values than expected, something must have gone wrong, throw an exception + if (arrays.Count != expectedValues) + { + throw new BitwardenException(100, $"Invalid number of return values. Expected {expectedValues}, got {arrays.Count}"); + } + + return arrays; } finally { - stateHandle.Free(); - registrationResponseHandle.Free(); + ffi.FreeHandles(); } } - - internal static byte[] FinishServerRegistration(CipherConfiguration config, byte[] registrationUpload) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var registrationUploadBuf = BuildBuffer(registrationUpload, out var handle); - try - { - var response = finish_server_registration(configStr, registrationUploadBuf); - return HandleResponse(response, 1)[0]; - } - finally - { - handle.Free(); - } - } - - internal static (byte[], byte[]) StartClientLogin(CipherConfiguration config, string password) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var response = start_client_login(configStr, password); - var ret = HandleResponse(response, 2); - return (ret[0], ret[1]); - } - - internal static (byte[], byte[]) StartServerLogin(CipherConfiguration config, byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var serverSetupBuf = BuildBuffer(serverSetup, out var serverSetupHandle); - var serverRegistrationBuf = BuildBuffer(serverRegistration, out var serverRegistrationHandle); - var credentialRequestBuf = BuildBuffer(credentialRequest, out var credentialRequestHandle); - try - { - var response = start_server_login(configStr, serverSetupBuf, serverRegistrationBuf, credentialRequestBuf, username); - var ret = HandleResponse(response, 2); - return (ret[0], ret[1]); - } - finally - { - serverSetupHandle.Free(); - serverRegistrationHandle.Free(); - credentialRequestHandle.Free(); - } - - } - - internal static (byte[], byte[], byte[], byte[]) FinishClientLogin(CipherConfiguration config, byte[] state, byte[] credentialResponse, string password) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var stateBuf = BuildBuffer(state, out var stateHandle); - var credentialResponseBuf = BuildBuffer(credentialResponse, out var credentialResponseHandle); - - try - { - var response = finish_client_login(configStr, stateBuf, credentialResponseBuf, password); - var ret = HandleResponse(response, 4); - return (ret[0], ret[1], ret[2], ret[3]); - } - finally - { - stateHandle.Free(); - credentialResponseHandle.Free(); - } - } - - internal static byte[] FinishServerLogin(CipherConfiguration config, byte[] state, byte[] credentialFinalization) - { - var configStr = JsonSerializer.Serialize(config, _serializerOptions); - var stateBuf = BuildBuffer(state, out var stateHandle); - var credentialFinalizationBuf = BuildBuffer(credentialFinalization, out var credentialFinalizationHandle); - try - { - var response = finish_server_login(configStr, stateBuf, credentialFinalizationBuf); - return HandleResponse(response, 1)[0]; - } - finally - { - stateHandle.Free(); - credentialFinalizationHandle.Free(); - } - - } - } diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs index b1371971..1d97d2f4 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs @@ -1,20 +1,20 @@ namespace Bitwarden.Opaque; #pragma warning disable CA1822 // Mark members as static -/// The result of +/// The result of public struct ClientRegistrationStartResult { - /// The registration response which is then passed to . + /// The registration response which is then passed to . public byte[] registrationRequest; - /// The client state, which must be kept on the client for . + /// The client state, which must be kept on the client for . public byte[] state; } -/// The result of +/// The result of public struct ClientRegistrationFinishResult { - /// The registration upload which is then passed to . + /// The registration upload which is then passed to . public byte[] registrationUpload; /// The export key output by client registration public byte[] exportKey; @@ -22,17 +22,25 @@ public struct ClientRegistrationFinishResult public byte[] serverSPKey; } +/// The result of public struct ClientLoginStartResult { + /// The credential request which is then passed to . public byte[] credentialRequest; + /// The state generated during the login start, which must be kept on the client for . public byte[] state; } +/// The result of public struct ClientLoginFinishResult { + /// The credential finalization which is then passed to . public byte[] credentialFinalization; + /// The session key generated after a successful login. public byte[] sessionKey; + /// The export key output by client login. public byte[] exportKey; + /// The server's static public key. public byte[] serverSPKey; } @@ -48,54 +56,79 @@ public sealed partial class BitwardenOpaqueClient /// public ClientRegistrationStartResult StartRegistration(CipherConfiguration config, string password) { - var (registrationRequest, state) = BitwardenLibrary.StartClientRegistration(config, password); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.start_client_registration(ffi.Cfg(config), password); + }, 2); return new ClientRegistrationStartResult { - registrationRequest = registrationRequest, - state = state + registrationRequest = result[0], + state = result[1] }; } /// - /// Finish the server registration process. This must happen after + /// Finish the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation - /// The state obtained from the client start operation, - /// The server registration response, + /// The state obtained from the client start operation, + /// The server registration response, /// The password to register /// public ClientRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] state, byte[] registrationResponse, string password) { - var (registrationUpload, exportKey, serverSPKey) = BitwardenLibrary.FinishClientRegistration(config, state, registrationResponse, password); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.finish_client_registration(ffi.Cfg(config), ffi.Buf(state), ffi.Buf(registrationResponse), password); + }, 3); return new ClientRegistrationFinishResult { - registrationUpload = registrationUpload, - exportKey = exportKey, - serverSPKey = serverSPKey + registrationUpload = result[0], + exportKey = result[1], + serverSPKey = result[2] }; } - + /// + /// Start the client login process. This is the first step in the login process. + /// + /// The Cipher configuration, must be the same for all the operation + /// The password to login + /// public ClientLoginStartResult StartLogin(CipherConfiguration config, string password) { - var (credentialRequest, state) = BitwardenLibrary.StartClientLogin(config, password); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.start_client_login(ffi.Cfg(config), password); + }, 2); return new ClientLoginStartResult { - credentialRequest = credentialRequest, - state = state + credentialRequest = result[0], + state = result[1] }; } + /// + /// Finish the client login process. This must happen after + /// + /// The Cipher configuration, must be the same for all the operation + /// The state obtained from the client start operation, + /// The server credential response, + /// The password to login + /// public ClientLoginFinishResult FinishLogin(CipherConfiguration config, byte[] state, byte[] credentialResponse, string password) { - var (credentialFinalization, sessionKey, exportKey, serverSPKey) = BitwardenLibrary.FinishClientLogin(config, state, credentialResponse, password); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.finish_client_login(ffi.Cfg(config), ffi.Buf(state), ffi.Buf(credentialResponse), password); + }, 4); return new ClientLoginFinishResult { - credentialFinalization = credentialFinalization, - sessionKey = sessionKey, - exportKey = exportKey, - serverSPKey = serverSPKey + credentialFinalization = result[0], + sessionKey = result[1], + exportKey = result[2], + serverSPKey = result[3] }; } } diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs index 16dc0101..8b954fa7 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs @@ -2,16 +2,16 @@ #pragma warning disable CA1822 // Mark members as static -/// The result of +/// The result of public struct ServerRegistrationStartResult { - /// The registration response which is then passed to . + /// The registration response which is then passed to . public byte[] registrationResponse; /// The server setup, which needs to be persisted on the server for future logins. public byte[] serverSetup; } -/// The result of +/// The result of public struct ServerRegistrationFinishResult { @@ -19,70 +19,103 @@ public struct ServerRegistrationFinishResult public byte[] serverRegistration; } +/// The result of public struct ServerLoginStartResult { + /// The credential response which is then passed to . public byte[] credentialResponse; + /// The state generated during the login start, which needs to be stored until the login finish. public byte[] state; } +/// The result of public struct ServerLoginFinishResult { + /// The session key generated after a successful login. public byte[] sessionKey; - } /// A class to represent server side functionality the Bitwarden OPAQUE library. public sealed partial class BitwardenOpaqueServer { /// - /// Start the server registration process. This must happen after + /// Start the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation /// The server setup. Use null to let the library create a new random one - /// The client registration request, + /// The client registration request, /// The username to register /// public ServerRegistrationStartResult StartRegistration(CipherConfiguration config, byte[]? serverSetup, byte[] registrationRequest, string username) { - var (registrationResponse, serverSetupRet) = BitwardenLibrary.StartServerRegistration(config, serverSetup, registrationRequest, username); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.start_server_registration(ffi.Cfg(config), ffi.Buf(serverSetup), ffi.Buf(registrationRequest), username); + }, 2); + return new ServerRegistrationStartResult { - registrationResponse = registrationResponse, - serverSetup = serverSetupRet + registrationResponse = result[0], + serverSetup = result[1] }; } /// - /// Finish the server registration process. This must happen after + /// Finish the server registration process. This must happen after /// /// The Cipher configuration, must be the same for all the operation - /// The client registration upload, + /// The client registration upload, /// public ServerRegistrationFinishResult FinishRegistration(CipherConfiguration config, byte[] registrationUpload) { - var serverRegistration = BitwardenLibrary.FinishServerRegistration(config, registrationUpload); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.finish_server_registration(ffi.Cfg(config), ffi.Buf(registrationUpload)); + }, 1); return new ServerRegistrationFinishResult { - serverRegistration = serverRegistration + serverRegistration = result[0] }; } + /// + /// Start the server login process. This must happen after + /// + /// The Cipher configuration, must be the same for all the operation + /// The server setup, previously generated or supplied during registration + /// The server registration, previously generated during registration + /// The client credential request, + /// The username to login + /// public ServerLoginStartResult StartLogin(CipherConfiguration config, byte[] serverSetup, byte[] serverRegistration, byte[] credentialRequest, string username) { - var (credentialResponse, state) = BitwardenLibrary.StartServerLogin(config, serverSetup, serverRegistration, credentialRequest, username); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.start_server_login(ffi.Cfg(config), ffi.Buf(serverSetup), ffi.Buf(serverRegistration), ffi.Buf(credentialRequest), username); + }, 2); return new ServerLoginStartResult { - credentialResponse = credentialResponse, - state = state + credentialResponse = result[0], + state = result[1] }; } + /// + /// Finish the server login process. This must happen after + /// + /// The Cipher configuration, must be the same for all the operation + /// The state generated during the login start, + /// The client credential finalization, + /// public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] state, byte[] credentialFinalization) { - var sessionKey = BitwardenLibrary.FinishServerLogin(config, state, credentialFinalization); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.finish_server_login(ffi.Cfg(config), ffi.Buf(state), ffi.Buf(credentialFinalization)); + }, 1); return new ServerLoginFinishResult { - sessionKey = sessionKey + sessionKey = result[0] }; } From 8dd4d1ecd8d425188a84fd71244b6e648c8984a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 18 Mar 2025 18:52:34 +0100 Subject: [PATCH 61/89] Optimize rust build, try to build windows arm --- .github/workflows/build-rust-cross-platform.yml | 12 ++++++++++-- extensions/Bitwarden.Opaque/rust/Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index b82f438f..4d301cea 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -20,6 +20,10 @@ jobs: - os: windows-2022 target: x86_64-pc-windows-msvc dotnet_rid: win-x64 + - os: ubuntu-22.04 + target: aarch64-pc-windows-msvc + dotnet_rid: win-arm64 + use_cross: true # caution: updating the linux runner OS version for GNU # targets will likely break the library for older OS versions. # prefer using oldest supported runner for for these targets @@ -29,15 +33,19 @@ jobs: - os: ubuntu-22.04 target: aarch64-unknown-linux-gnu dotnet_rid: linux-arm64 + use_cross: true - os: ubuntu-22.04 target: armv7-unknown-linux-gnueabihf dotnet_rid: linux-arm + use_cross: true - os: ubuntu-22.04 target: arm-unknown-linux-gnueabihf dotnet_rid: linux-armv6 + use_cross: true - os: ubuntu-22.04 target: armv5te-unknown-linux-gnueabi dotnet_rid: linux-armel + use_cross: true steps: - name: Checkout @@ -60,7 +68,7 @@ jobs: # Build Rust natively - name: Build Rust native for - ${{ matrix.settings.target }} - if: ${{ !startsWith(matrix.settings.os, 'ubuntu') || startsWith(matrix.settings.target, 'x86_64') }} + if: ${{ matrix.settings.use_cross !== true }} env: RUSTFLAGS: "-D warnings" MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems @@ -69,7 +77,7 @@ jobs: # Build Rust using cross - name: Build Rust cross for - ${{ matrix.settings.target }} - if: ${{ startsWith(matrix.settings.os, 'ubuntu') && !startsWith(matrix.settings.target, 'x86_64') }} + if: ${{ matrix.settings.use_cross === true }} env: RUSTFLAGS: "-D warnings" run: cross build --target ${{ matrix.settings.target }} --release diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.toml b/extensions/Bitwarden.Opaque/rust/Cargo.toml index 7a7f1fa7..98382df5 100644 --- a/extensions/Bitwarden.Opaque/rust/Cargo.toml +++ b/extensions/Bitwarden.Opaque/rust/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] argon2 = "0.5.3" digest = "0.10.7" generic-array = "0.14.7" -opaque-ke = { version = "3.0.0", features = ["std", "argon2", "curve25519"] } +opaque-ke = { version = "3.0.0", features = ["std", "argon2"] } rand = "0.8.5" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" @@ -20,5 +20,5 @@ zeroizing-alloc = "0.1.0" # Turn on LTO on release mode [profile.release] -lto = "thin" +lto = true codegen-units = 1 From eff7d464651efa28371969cb4e05af5d2e36a9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 18 Mar 2025 18:54:47 +0100 Subject: [PATCH 62/89] Fix condition --- .github/workflows/build-rust-cross-platform.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 4d301cea..55465578 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -68,7 +68,7 @@ jobs: # Build Rust natively - name: Build Rust native for - ${{ matrix.settings.target }} - if: ${{ matrix.settings.use_cross !== true }} + if: ${{ matrix.settings.use_cross != true }} env: RUSTFLAGS: "-D warnings" MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems @@ -77,7 +77,7 @@ jobs: # Build Rust using cross - name: Build Rust cross for - ${{ matrix.settings.target }} - if: ${{ matrix.settings.use_cross === true }} + if: ${{ matrix.settings.use_cross == true }} env: RUSTFLAGS: "-D warnings" run: cross build --target ${{ matrix.settings.target }} --release From 4a5ce35dab21c167c7ba68944b7ae49f6f3cdd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 18 Mar 2025 18:59:55 +0100 Subject: [PATCH 63/89] Try native build --- .github/workflows/build-rust-cross-platform.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml index 55465578..845a13c3 100644 --- a/.github/workflows/build-rust-cross-platform.yml +++ b/.github/workflows/build-rust-cross-platform.yml @@ -17,13 +17,15 @@ jobs: - os: macos-13 target: aarch64-apple-darwin dotnet_rid: osx-arm64 + - os: windows-2022 + target: i686-pc-windows-msvc + dotnet_rid: win-x86 - os: windows-2022 target: x86_64-pc-windows-msvc dotnet_rid: win-x64 - - os: ubuntu-22.04 + - os: windows-2022 target: aarch64-pc-windows-msvc dotnet_rid: win-arm64 - use_cross: true # caution: updating the linux runner OS version for GNU # targets will likely break the library for older OS versions. # prefer using oldest supported runner for for these targets From 685473747568a5fa278ba22f1129789ea1e5a0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 18 Mar 2025 19:15:03 +0100 Subject: [PATCH 64/89] Update pack action to cross build libs --- .github/workflows/build.yml | 7 ------- .github/workflows/pack-and-release.yml | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e0b878b..08f94c31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,16 +25,9 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - name: Install rust - uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable - with: - toolchain: stable - - name: Build solution run: dotnet build - # TODO: Testing OPAQUE building, remove this when done and add it to pack-and-release.yml? - - name: Download cross compiled library files uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: diff --git a/.github/workflows/pack-and-release.yml b/.github/workflows/pack-and-release.yml index 41a1c567..3ea66727 100644 --- a/.github/workflows/pack-and-release.yml +++ b/.github/workflows/pack-and-release.yml @@ -11,8 +11,14 @@ on: description: Token used to publish packages jobs: + build_rust: + name: Build Rust Cross Platform + uses: ./.github/workflows/build-rust-cross-platform.yml + release: name: Release + needs: + - build_rust runs-on: ubuntu-22.04 outputs: package: ${{ steps.parse-package.outputs.result }} @@ -23,11 +29,6 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - name: Install rust - uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable - with: - toolchain: stable - - name: Parse package id: parse-package uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 @@ -52,6 +53,12 @@ jobs: return refParts[3]; + - name: Download cross compiled library files + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: libopaque_ke_binding_all_files + path: extensions/Bitwarden.Opaque/rust/dist/ + - name: Pack run: dotnet pack -c Release -p:IsPreRelease=$IS_PRERELEASE working-directory: "./extensions/${{ steps.parse-package.outputs.result }}/src" From 3a2009e3548b52361b082bc431025628351a0458 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:19:36 +0100 Subject: [PATCH 65/89] Bump Bitwarden.Opaque version to 0.1.0 (#107) Co-authored-by: github-actions --- extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj index 2824de79..a12e0e40 100644 --- a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj +++ b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj @@ -31,9 +31,9 @@ bitwarden.png Bitwarden;Opaque;PAKE;.NET - 0.0.1 + 0.1.0 beta - 2 + 1 $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) From e47cec781bc391ff11d89a550be1f03354ca44c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Tue, 18 Mar 2025 19:34:30 +0100 Subject: [PATCH 66/89] Temp fix for prerelease --- .github/workflows/prerelease.yml | 2 +- extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index c20496f7..393a57ac 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -8,7 +8,7 @@ on: jobs: prerelease: name: Do prerelease - uses: bitwarden/dotnet-extensions/.github/workflows/pack-and-release.yml@main + uses: ./.github/workflows/pack-and-release.yml with: prerelease: true secrets: diff --git a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj index a12e0e40..32a9c0cc 100644 --- a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj +++ b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj @@ -33,7 +33,7 @@ 0.1.0 beta - 1 + 2 $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) From abdb9f2b3a4ac349500f8c9881d3b17f6d7c3f39 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 19 Mar 2025 07:05:40 +0100 Subject: [PATCH 67/89] Add seeded registration --- extensions/Bitwarden.Opaque/rust/Cargo.lock | 8 +++++ extensions/Bitwarden.Opaque/rust/Cargo.toml | 2 ++ .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 28 +++++++++++++++ extensions/Bitwarden.Opaque/rust/src/lib.rs | 28 ++++++++++++++- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 36 +++++++++++-------- .../Bitwarden.Opaque/rust/src/opaque/types.rs | 34 ++++++++++++++++-- .../Bitwarden.Opaque/src/BitwardenLibrary.cs | 12 +++++++ .../src/BitwardenOpaqueServer.cs | 5 +++ 8 files changed, 135 insertions(+), 18 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.lock b/extensions/Bitwarden.Opaque/rust/Cargo.lock index ca8596be..3fea5e1e 100644 --- a/extensions/Bitwarden.Opaque/rust/Cargo.lock +++ b/extensions/Bitwarden.Opaque/rust/Cargo.lock @@ -233,6 +233,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -321,8 +327,10 @@ dependencies = [ "argon2", "digest", "generic-array", + "hex", "opaque-ke", "rand", + "rand_chacha", "serde", "serde_json", "voprf", diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.toml b/extensions/Bitwarden.Opaque/rust/Cargo.toml index 7a7f1fa7..abc62fa3 100644 --- a/extensions/Bitwarden.Opaque/rust/Cargo.toml +++ b/extensions/Bitwarden.Opaque/rust/Cargo.toml @@ -11,8 +11,10 @@ crate-type = ["cdylib"] argon2 = "0.5.3" digest = "0.10.7" generic-array = "0.14.7" +hex = "0.4.3" opaque-ke = { version = "3.0.0", features = ["std", "argon2", "curve25519"] } rand = "0.8.5" +rand_chacha = "0.3.1" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" voprf = "0.5.0" diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index 017755f3..e6a570bc 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -8,12 +8,40 @@ use crate::{ mod types; +use rand::RngCore; use types::*; +#[cfg(test)] +pub use types::Buffer; + unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a str, Error> { unsafe { std::ffi::CStr::from_ptr(input).to_str() }.map_err(|_| Error::InvalidInput(name.into())) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response { + let seed = try_ffi!(unsafe { seed.as_slice() }); + if seed.len() != 32 { + try_ffi!(Err(Error::InvalidInput("Seed must be 32 bytes".into()))); + } + let seed = try_ffi!(<[u8; 32]>::try_from(seed).map_err(|_| Error::InvalidInput("Seed must be 32 bytes".into()))); + let config = CipherConfiguration::fake_from_seed(seed); + + let rng = try_ffi!(config.rng.as_ref().ok_or(Error::InvalidInput("No rng found".into()))); + let mut password: [u8; 32] = [0; 32]; + rng.borrow_mut().fill_bytes(&mut password); + let password = hex::encode(password); + let mut username: [u8; 32] = [0; 32]; + rng.borrow_mut().fill_bytes(&mut username); + let username = hex::encode(username); + + let start = try_ffi!(config.start_client_registration(&password.as_str())); + let server_start = try_ffi!(config.start_server_registration(None, &start.registration_request, &username)); + let client_finish = try_ffi!(config.finish_client_registration(&start.state, &server_start.registration_response, &password)); + let server_finish = try_ffi!(config.finish_server_registration(&client_finish.registration_upload)); + Response::ok2(server_start.server_setup, server_finish.server_registration) +} + /// /// /// # Safety diff --git a/extensions/Bitwarden.Opaque/rust/src/lib.rs b/extensions/Bitwarden.Opaque/rust/src/lib.rs index f9f09126..8c4aff2f 100644 --- a/extensions/Bitwarden.Opaque/rust/src/lib.rs +++ b/extensions/Bitwarden.Opaque/rust/src/lib.rs @@ -23,7 +23,7 @@ impl From for Error { #[cfg(test)] mod tests { - use crate::opaque::*; + use crate::{ffi::{self, register_seeded_fake_config}, opaque::*}; #[test] fn test() { @@ -82,4 +82,30 @@ mod tests { let _ = server_login_finish_result.session_key; } + + #[test] + fn test_seeded() { + let seed = [0u8; 32]; + let buffer = ffi::Buffer::from_vec(seed.to_vec()); + let res = unsafe { register_seeded_fake_config(buffer) }; + assert!(res.error == 0); + let (server_setup, password_file) = unsafe { (res.data1.as_slice().unwrap(), res.data2.as_slice().unwrap()) }; + assert_eq!(server_setup.len(), 128); + assert_eq!(password_file.len(), 192); + + let password = "password"; + let username = "username"; + let config = CipherConfiguration::default(); + let res = config.start_client_login(password).unwrap(); + let server_res = config + .start_server_login( + server_setup, + password_file, + &res.credential_request, + username, + ) + .unwrap(); + let res = config.finish_client_login(&res.state, &server_res.credential_response, password); + assert!(res.is_err()); + } } diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index d95dc609..649e9688 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -1,6 +1,8 @@ -use argon2::Argon2; +use std::cell::RefCell; + use opaque_ke::*; use rand::rngs::OsRng; +use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use crate::Error; @@ -59,6 +61,7 @@ pub trait OpaqueUtil: Sized { type Output; fn as_variant(config: &CipherConfiguration) -> Option; fn get_ksf(&self) -> Result; + fn get_rng(&self) -> RefCell; } fn invalid_config(config: &CipherConfiguration) -> Error { @@ -157,15 +160,17 @@ impl OpaqueImpl for CipherConfiguration { } // Define the cipher suite and implement the required traits on it (opaque_ke::CipherSuite+OpaqueUtil+OpaqueImpl) -struct RistrettoTripleDhArgonSuite(Argon2id); +struct RistrettoTripleDhArgonSuite(RefCell); impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { type OprfCs = opaque_ke::Ristretto255; type KeGroup = opaque_ke::Ristretto255; type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; - type Ksf = argon2::Argon2<'static>; + // this is run on only the client side anyways. Using an identity here means the test vectors do not match when using client functions + // from this binding. + type Ksf = IdentityKsf; } impl OpaqueUtil for RistrettoTripleDhArgonSuite { - type Output = argon2::Argon2<'static>; + type Output = IdentityKsf; fn as_variant(config: &CipherConfiguration) -> Option { match config { @@ -174,18 +179,18 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { oprf_cs: OprfCs::Ristretto255, ke_group: KeGroup::Ristretto255, key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => Some(Self(*argon)), + ksf: _, + rng, + } => Some(Self(rng.as_ref().unwrap_or(&RefCell::from(ChaCha20Rng::from_entropy())).clone())), _ => None, } } fn get_ksf(&self) -> Result { - Ok(Argon2::new( - argon2::Algorithm::Argon2id, - argon2::Version::V0x13, - argon2::Params::new(self.0.memory, self.0.iterations, self.0.parallelism, None) - .map_err(|_| Error::InvalidConfig("Invalid Argon2 parameters".into()))?, - )) + Ok(IdentityKsf { }) + } + + fn get_rng(&self) -> RefCell { + self.0.clone() } } @@ -210,7 +215,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { ) -> Result { let server_setup = match server_setup { Some(server_setup) => ServerSetup::::deserialize(server_setup)?, - None => ServerSetup::::new(&mut OsRng), + None => ServerSetup::::new(self.get_rng().get_mut()), }; let result = ServerRegistration::start( &server_setup, @@ -229,11 +234,12 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { password: &str, ) -> Result { let state = ClientRegistration::::deserialize(state)?; + let ksf = self.get_ksf()?; let result = state.finish( - &mut OsRng, + self.get_rng().get_mut(), password.as_bytes(), RegistrationResponse::deserialize(registration_response)?, - ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&self.get_ksf()?)), + ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)), )?; Ok(types::ClientRegistrationFinishResult { registration_upload: result.message.serialize().to_vec(), diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs index 4fe9d2e5..0e169bed 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs @@ -1,5 +1,9 @@ -use std::str::FromStr; +use std::{cell::RefCell, str::FromStr}; +use generic_array::{ArrayLength, GenericArray}; +use opaque_ke::errors::InternalError; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; use serde::{Deserialize, Serialize}; use crate::Error; @@ -38,7 +42,7 @@ pub struct Argon2id { pub parallelism: u32, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CipherConfiguration { pub opaque_version: u32, @@ -47,6 +51,18 @@ pub struct CipherConfiguration { pub ke_group: KeGroup, pub key_exchange: KeyExchange, pub ksf: Ksf, + + #[serde(skip)] + pub(crate) rng: Option>, +} + +impl CipherConfiguration { + pub(crate) fn fake_from_seed(seed: [u8; 32]) -> Self { + Self { + rng: Some(RefCell::from(ChaCha20Rng::from_seed(seed))), + ..Default::default() + } + } } impl Default for CipherConfiguration { @@ -61,6 +77,7 @@ impl Default for CipherConfiguration { iterations: 4, parallelism: 4, }), + rng: Some(RefCell::from(ChaCha20Rng::from_entropy())), } } } @@ -118,3 +135,16 @@ pub(crate) struct ClientLoginFinishResult { pub(crate) struct ServerLoginFinishResult { pub(crate) session_key: Vec, } + +#[derive(Debug, Default)] +pub(crate) struct IdentityKsf { +} + +impl opaque_ke::ksf::Ksf for IdentityKsf { + fn hash>( + &self, + input: GenericArray, + ) -> Result, InternalError> { + Ok(input) + } +} \ No newline at end of file diff --git a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs index 279f0843..1acee22f 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs @@ -40,6 +40,9 @@ private struct Response [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial void free_buffer(Buffer buf); + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] + private static partial Response register_seeded_fake_config(Buffer seed); + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] private static partial Response start_client_registration(string config, string password); @@ -121,6 +124,15 @@ private static List HandleResponse(Response response, int expectedValues PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + internal static (byte[], byte[]) RegisterSeededFakeConfig(byte[] seed) + { + var response = register_seeded_fake_config(BuildBuffer(seed, out var handle)); + try { + var responseValues = HandleResponse(response, 2); + return (responseValues[0], responseValues[1]); + } finally { handle.Free(); } + } + internal static (byte[], byte[]) StartClientRegistration(CipherConfiguration config, string password) { var configStr = JsonSerializer.Serialize(config, _serializerOptions); diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs index 16dc0101..4defb8e6 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs @@ -86,4 +86,9 @@ public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] st }; } + public (byte[] serverSetup, byte[] serverRegistration) SeededFakeRegistration(byte[] seed) + { + return BitwardenLibrary.RegisterSeededFakeConfig(seed); + } + } From 26f3f09a5ba374cdea6c0ea2dc796a875782e0bf Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 19 Mar 2025 07:27:21 +0100 Subject: [PATCH 68/89] Fix build --- extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs index 19136562..f0254d2d 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs @@ -121,7 +121,11 @@ public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] st public (byte[] serverSetup, byte[] serverRegistration) SeededFakeRegistration(byte[] seed) { - return BitwardenLibrary.RegisterSeededFakeConfig(seed); + var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => + { + return BitwardenLibrary.register_seeded_fake_config(ffi.Buf(seed)); + }, 1); + return (result[0], result[1]); } } From d7fbc2c2e6ae17a552345d3e2e151b4ae1ddb95d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 19 Mar 2025 07:34:43 +0100 Subject: [PATCH 69/89] Replace osrng --- extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index 649e9688..822b1720 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -1,7 +1,6 @@ use std::cell::RefCell; use opaque_ke::*; -use rand::rngs::OsRng; use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; use crate::Error; @@ -201,7 +200,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { &self, password: &str, ) -> Result { - let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; + let result = ClientRegistration::::start(self.get_rng().get_mut(), password.as_bytes())?; Ok(types::ClientRegistrationStartResult { registration_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), @@ -260,7 +259,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { } fn start_client_login(&self, password: &str) -> Result { - let result = ClientLogin::::start(&mut OsRng, password.as_bytes())?; + let result = ClientLogin::::start(self.get_rng().get_mut(), password.as_bytes())?; Ok(types::ClientLoginStartResult { credential_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), @@ -274,7 +273,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { username: &str, ) -> Result { let result = ServerLogin::start( - &mut OsRng, + self.get_rng().get_mut(), &ServerSetup::::deserialize(server_setup)?, Some(ServerRegistration::::deserialize( server_registration, From 7b7f76fef2140f9aa4eb543418a4485478c5db7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 11:43:07 +0100 Subject: [PATCH 70/89] Fix number of return values, make RNG non-optional and return a result to avoid panics --- .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 34 ++++-------- extensions/Bitwarden.Opaque/rust/src/lib.rs | 12 ++--- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 53 ++++++++++++++----- .../Bitwarden.Opaque/rust/src/opaque/types.rs | 19 ++++--- .../src/BitwardenOpaqueServer.cs | 2 +- extensions/Bitwarden.Opaque/tests/Tests.cs | 19 +++++++ 6 files changed, 87 insertions(+), 52 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index e6a570bc..87031cd8 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -8,38 +8,24 @@ use crate::{ mod types; -use rand::RngCore; use types::*; -#[cfg(test)] -pub use types::Buffer; - unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a str, Error> { - unsafe { std::ffi::CStr::from_ptr(input).to_str() }.map_err(|_| Error::InvalidInput(name.into())) + unsafe { std::ffi::CStr::from_ptr(input).to_str() } + .map_err(|_| Error::InvalidInput(name.into())) } #[unsafe(no_mangle)] pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response { let seed = try_ffi!(unsafe { seed.as_slice() }); - if seed.len() != 32 { - try_ffi!(Err(Error::InvalidInput("Seed must be 32 bytes".into()))); - } - let seed = try_ffi!(<[u8; 32]>::try_from(seed).map_err(|_| Error::InvalidInput("Seed must be 32 bytes".into()))); - let config = CipherConfiguration::fake_from_seed(seed); - - let rng = try_ffi!(config.rng.as_ref().ok_or(Error::InvalidInput("No rng found".into()))); - let mut password: [u8; 32] = [0; 32]; - rng.borrow_mut().fill_bytes(&mut password); - let password = hex::encode(password); - let mut username: [u8; 32] = [0; 32]; - rng.borrow_mut().fill_bytes(&mut username); - let username = hex::encode(username); - - let start = try_ffi!(config.start_client_registration(&password.as_str())); - let server_start = try_ffi!(config.start_server_registration(None, &start.registration_request, &username)); - let client_finish = try_ffi!(config.finish_client_registration(&start.state, &server_start.registration_response, &password)); - let server_finish = try_ffi!(config.finish_server_registration(&client_finish.registration_upload)); - Response::ok2(server_start.server_setup, server_finish.server_registration) + let seed = try_ffi!( + <[u8; 32]>::try_from(seed).map_err(|_| Error::InvalidInput("Seed must be 32 bytes".into())) + ); + + let (server_setup, server_registration) = + try_ffi!(crate::opaque::register_seeded_fake_config(seed)); + + Response::ok2(server_setup, server_registration) } /// diff --git a/extensions/Bitwarden.Opaque/rust/src/lib.rs b/extensions/Bitwarden.Opaque/rust/src/lib.rs index 8c4aff2f..e12cdac3 100644 --- a/extensions/Bitwarden.Opaque/rust/src/lib.rs +++ b/extensions/Bitwarden.Opaque/rust/src/lib.rs @@ -23,7 +23,7 @@ impl From for Error { #[cfg(test)] mod tests { - use crate::{ffi::{self, register_seeded_fake_config}, opaque::*}; + use crate::opaque::*; #[test] fn test() { @@ -86,10 +86,8 @@ mod tests { #[test] fn test_seeded() { let seed = [0u8; 32]; - let buffer = ffi::Buffer::from_vec(seed.to_vec()); - let res = unsafe { register_seeded_fake_config(buffer) }; - assert!(res.error == 0); - let (server_setup, password_file) = unsafe { (res.data1.as_slice().unwrap(), res.data2.as_slice().unwrap()) }; + let (server_setup, password_file) = + super::opaque::register_seeded_fake_config(seed).unwrap(); assert_eq!(server_setup.len(), 128); assert_eq!(password_file.len(), 192); @@ -99,8 +97,8 @@ mod tests { let res = config.start_client_login(password).unwrap(); let server_res = config .start_server_login( - server_setup, - password_file, + &server_setup, + &password_file, &res.credential_request, username, ) diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index 822b1720..bdf15ffd 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -1,7 +1,7 @@ -use std::cell::RefCell; +use std::cell::{RefCell, RefMut}; use opaque_ke::*; -use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; +use rand_chacha::ChaCha20Rng; use crate::Error; @@ -60,7 +60,7 @@ pub trait OpaqueUtil: Sized { type Output; fn as_variant(config: &CipherConfiguration) -> Option; fn get_ksf(&self) -> Result; - fn get_rng(&self) -> RefCell; + fn get_rng(&self) -> Result, Error>; } fn invalid_config(config: &CipherConfiguration) -> Error { @@ -180,16 +180,18 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { key_exchange: KeyExchange::TripleDh, ksf: _, rng, - } => Some(Self(rng.as_ref().unwrap_or(&RefCell::from(ChaCha20Rng::from_entropy())).clone())), + } => Some(Self(rng.clone())), _ => None, } } fn get_ksf(&self) -> Result { - Ok(IdentityKsf { }) + Ok(IdentityKsf {}) } - fn get_rng(&self) -> RefCell { - self.0.clone() + fn get_rng(&self) -> Result, Error> { + self.0 + .try_borrow_mut() + .map_err(|e| Error::InvalidConfig(e.to_string())) } } @@ -200,7 +202,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { &self, password: &str, ) -> Result { - let result = ClientRegistration::::start(self.get_rng().get_mut(), password.as_bytes())?; + let result = ClientRegistration::::start(&mut *self.get_rng()?, password.as_bytes())?; Ok(types::ClientRegistrationStartResult { registration_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), @@ -214,7 +216,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { ) -> Result { let server_setup = match server_setup { Some(server_setup) => ServerSetup::::deserialize(server_setup)?, - None => ServerSetup::::new(self.get_rng().get_mut()), + None => ServerSetup::::new(&mut *self.get_rng()?), }; let result = ServerRegistration::start( &server_setup, @@ -235,7 +237,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { let state = ClientRegistration::::deserialize(state)?; let ksf = self.get_ksf()?; let result = state.finish( - self.get_rng().get_mut(), + &mut *self.get_rng()?, password.as_bytes(), RegistrationResponse::deserialize(registration_response)?, ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)), @@ -259,7 +261,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { } fn start_client_login(&self, password: &str) -> Result { - let result = ClientLogin::::start(self.get_rng().get_mut(), password.as_bytes())?; + let result = ClientLogin::::start(&mut *self.get_rng()?, password.as_bytes())?; Ok(types::ClientLoginStartResult { credential_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), @@ -273,7 +275,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { username: &str, ) -> Result { let result = ServerLogin::start( - self.get_rng().get_mut(), + &mut *self.get_rng()?, &ServerSetup::::deserialize(server_setup)?, Some(ServerRegistration::::deserialize( server_registration, @@ -321,6 +323,33 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { } } +pub fn register_seeded_fake_config(seed: [u8; 32]) -> Result<(Vec, Vec), Error> { + use rand::RngCore as _; + + let config = CipherConfiguration::fake_from_seed(seed); + + let mut password: [u8; 32] = [0; 32]; + let mut username: [u8; 32] = [0; 32]; + { + let mut rng = config.rng.borrow_mut(); + rng.fill_bytes(&mut password); + rng.fill_bytes(&mut username); + } + let password = hex::encode(password); + let username = hex::encode(username); + + let start = config.start_client_registration(password.as_str())?; + let server_start = + config.start_server_registration(None, &start.registration_request, &username)?; + let client_finish = config.finish_client_registration( + &start.state, + &server_start.registration_response, + &password, + )?; + let server_finish = config.finish_server_registration(&client_finish.registration_upload)?; + Ok((server_start.server_setup, server_finish.server_registration)) +} + /* TODO: If in the future we add more cipher suites, we will need to create a new OpaqueImpl implementation and add it to the CipherConfiguration matches. diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs index 0e169bed..281e1529 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs @@ -52,14 +52,18 @@ pub struct CipherConfiguration { pub key_exchange: KeyExchange, pub ksf: Ksf, - #[serde(skip)] - pub(crate) rng: Option>, + #[serde(skip, default = "default_rng")] + pub(crate) rng: RefCell, +} + +fn default_rng() -> RefCell { + RefCell::from(ChaCha20Rng::from_entropy()) } impl CipherConfiguration { pub(crate) fn fake_from_seed(seed: [u8; 32]) -> Self { Self { - rng: Some(RefCell::from(ChaCha20Rng::from_seed(seed))), + rng: RefCell::from(ChaCha20Rng::from_seed(seed)), ..Default::default() } } @@ -77,7 +81,7 @@ impl Default for CipherConfiguration { iterations: 4, parallelism: 4, }), - rng: Some(RefCell::from(ChaCha20Rng::from_entropy())), + rng: default_rng(), } } } @@ -137,8 +141,7 @@ pub(crate) struct ServerLoginFinishResult { } #[derive(Debug, Default)] -pub(crate) struct IdentityKsf { -} +pub(crate) struct IdentityKsf {} impl opaque_ke::ksf::Ksf for IdentityKsf { fn hash>( @@ -146,5 +149,5 @@ impl opaque_ke::ksf::Ksf for IdentityKsf { input: GenericArray, ) -> Result, InternalError> { Ok(input) - } -} \ No newline at end of file + } +} diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs index f0254d2d..0df816a7 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs @@ -124,7 +124,7 @@ public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] st var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => { return BitwardenLibrary.register_seeded_fake_config(ffi.Buf(seed)); - }, 1); + }, 2); return (result[0], result[1]); } diff --git a/extensions/Bitwarden.Opaque/tests/Tests.cs b/extensions/Bitwarden.Opaque/tests/Tests.cs index 55817728..c519cad1 100644 --- a/extensions/Bitwarden.Opaque/tests/Tests.cs +++ b/extensions/Bitwarden.Opaque/tests/Tests.cs @@ -4,6 +4,25 @@ namespace Bitwarden.Opaque.Tests; public class SampleTests { + [Fact] + public void TestSeededRegistration() + { + var server = new BitwardenOpaqueServer(); + + var seed = new byte[32]; + var (serverSetup1, serverRegistration1) = server.SeededFakeRegistration(seed); + + Assert.NotNull(serverSetup1); + Assert.NotNull(serverRegistration1); + + var (serverSetup2, serverRegistration2) = server.SeededFakeRegistration(seed); + Assert.NotNull(serverSetup2); + Assert.NotNull(serverRegistration2); + + Assert.Equal(serverSetup1, serverSetup2); + Assert.Equal(serverRegistration1, serverRegistration2); + } + [Fact] public void TestRegistration() { From cf4d77e484c28fbab8c755f3318a784c5f1d9dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 11:54:43 +0100 Subject: [PATCH 71/89] Use mutable functions in trait to avoid refcell --- .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 16 +-- extensions/Bitwarden.Opaque/rust/src/lib.rs | 4 +- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 104 +++++++++--------- .../Bitwarden.Opaque/rust/src/opaque/types.rs | 10 +- 4 files changed, 68 insertions(+), 66 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index 87031cd8..33a2ee9b 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -40,7 +40,7 @@ pub unsafe extern "C" fn start_client_registration( let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); let password: &str = try_ffi!(unsafe { parse_str(password, "password") }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let result = try_ffi!(config.start_client_registration(password)); @@ -63,7 +63,7 @@ pub unsafe extern "C" fn start_server_registration( let registration_request = try_ffi!(unsafe { registration_request.as_slice() }); let username = try_ffi!(unsafe { parse_str(username, "username") }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.start_server_registration(server_setup, registration_request, username)); @@ -87,7 +87,7 @@ pub unsafe extern "C" fn finish_client_registration( let state = try_ffi!(unsafe { state.as_slice() }); let password = try_ffi!(unsafe { parse_str(password, "password") }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_client_registration(state, registration_response, password)); @@ -111,7 +111,7 @@ pub unsafe extern "C" fn finish_server_registration( let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); let registration_upload = try_ffi!(unsafe { registration_upload.as_slice() }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_server_registration(registration_upload)); Response::ok1(response.server_registration) @@ -129,7 +129,7 @@ pub unsafe extern "C" fn start_client_login( let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); let password = try_ffi!(unsafe { parse_str(password, "password") }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.start_client_login(password)); Response::ok2(response.credential_request, response.state) @@ -153,7 +153,7 @@ pub unsafe extern "C" fn start_server_login( let credential_request = try_ffi!(unsafe { credential_request.as_slice() }); let username = try_ffi!(unsafe { parse_str(username, "username") }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.start_server_login( server_setup, @@ -180,7 +180,7 @@ pub unsafe extern "C" fn finish_client_login( let credential_response = try_ffi!(unsafe { credential_response.as_slice() }); let password = try_ffi!(unsafe { parse_str(password, "password") }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_client_login(state, credential_response, password)); Response::ok4( @@ -205,7 +205,7 @@ pub unsafe extern "C" fn finish_server_login( let state = try_ffi!(unsafe { state.as_slice() }); let credential_finalization = try_ffi!(unsafe { credential_finalization.as_slice() }); - let config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_server_login(state, credential_finalization)); diff --git a/extensions/Bitwarden.Opaque/rust/src/lib.rs b/extensions/Bitwarden.Opaque/rust/src/lib.rs index e12cdac3..dac6be27 100644 --- a/extensions/Bitwarden.Opaque/rust/src/lib.rs +++ b/extensions/Bitwarden.Opaque/rust/src/lib.rs @@ -30,7 +30,7 @@ mod tests { let password = "password"; let username = "username"; - let config = CipherConfiguration::default(); + let mut config = CipherConfiguration::default(); // Registration @@ -93,7 +93,7 @@ mod tests { let password = "password"; let username = "username"; - let config = CipherConfiguration::default(); + let mut config = CipherConfiguration::default(); let res = config.start_client_login(password).unwrap(); let server_res = config .start_server_login( diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index bdf15ffd..49c82feb 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -1,5 +1,3 @@ -use std::cell::{RefCell, RefMut}; - use opaque_ke::*; use rand_chacha::ChaCha20Rng; @@ -13,42 +11,45 @@ pub(crate) use types::*; // This trait implements dynamic dispatch to allow using opaque-ke without generics. pub trait OpaqueImpl { fn start_client_registration( - &self, + &mut self, password: &str, ) -> Result; fn start_server_registration( - &self, + &mut self, server_setup: Option<&[u8]>, registration_request: &[u8], username: &str, ) -> Result; fn finish_client_registration( - &self, + &mut self, state: &[u8], registration_response: &[u8], password: &str, ) -> Result; fn finish_server_registration( - &self, + &mut self, registration_upload: &[u8], ) -> Result; - fn start_client_login(&self, password: &str) -> Result; + fn start_client_login( + &mut self, + password: &str, + ) -> Result; fn start_server_login( - &self, + &mut self, server_setup: &[u8], server_registration: &[u8], credential_request: &[u8], username: &str, ) -> Result; fn finish_client_login( - &self, + &mut self, state: &[u8], credential_response: &[u8], password: &str, ) -> Result; fn finish_server_login( - &self, + &mut self, state: &[u8], credential_finalization: &[u8], ) -> Result; @@ -60,7 +61,7 @@ pub trait OpaqueUtil: Sized { type Output; fn as_variant(config: &CipherConfiguration) -> Option; fn get_ksf(&self) -> Result; - fn get_rng(&self) -> Result, Error>; + fn get_rng(&mut self) -> &mut ChaCha20Rng; } fn invalid_config(config: &CipherConfiguration) -> Error { @@ -70,10 +71,10 @@ fn invalid_config(config: &CipherConfiguration) -> Error { // Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. impl OpaqueImpl for CipherConfiguration { fn start_client_registration( - &self, + &mut self, password: &str, ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_client_registration(password); }; Err(Error::InvalidConfig( @@ -81,51 +82,54 @@ impl OpaqueImpl for CipherConfiguration { )) } fn start_server_registration( - &self, + &mut self, server_setup: Option<&[u8]>, registration_request: &[u8], username: &str, ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_server_registration(server_setup, registration_request, username); }; Err(invalid_config(self)) } fn finish_client_registration( - &self, + &mut self, state: &[u8], registration_response: &[u8], password: &str, ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_client_registration(state, registration_response, password); }; Err(invalid_config(self)) } fn finish_server_registration( - &self, + &mut self, registration_upload: &[u8], ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_server_registration(registration_upload); }; Err(invalid_config(self)) } - fn start_client_login(&self, password: &str) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + fn start_client_login( + &mut self, + password: &str, + ) -> Result { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_client_login(password); }; Err(invalid_config(self)) } fn start_server_login( - &self, + &mut self, server_setup: &[u8], server_registration: &[u8], credential_request: &[u8], username: &str, ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.start_server_login( server_setup, server_registration, @@ -136,22 +140,22 @@ impl OpaqueImpl for CipherConfiguration { Err(invalid_config(self)) } fn finish_client_login( - &self, + &mut self, state: &[u8], credential_response: &[u8], password: &str, ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_client_login(state, credential_response, password); }; Err(invalid_config(self)) } fn finish_server_login( - &self, + &mut self, state: &[u8], credential_finalization: &[u8], ) -> Result { - if let Some(suite) = RistrettoTripleDhArgonSuite::as_variant(self) { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { return suite.finish_server_login(state, credential_finalization); }; Err(invalid_config(self)) @@ -159,7 +163,7 @@ impl OpaqueImpl for CipherConfiguration { } // Define the cipher suite and implement the required traits on it (opaque_ke::CipherSuite+OpaqueUtil+OpaqueImpl) -struct RistrettoTripleDhArgonSuite(RefCell); +struct RistrettoTripleDhArgonSuite(ChaCha20Rng); impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { type OprfCs = opaque_ke::Ristretto255; type KeGroup = opaque_ke::Ristretto255; @@ -188,10 +192,8 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { Ok(IdentityKsf {}) } - fn get_rng(&self) -> Result, Error> { - self.0 - .try_borrow_mut() - .map_err(|e| Error::InvalidConfig(e.to_string())) + fn get_rng(&mut self) -> &mut ChaCha20Rng { + &mut self.0 } } @@ -199,24 +201,24 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { // If we need to add more cipher suites, we will need to copy this implementation over, or ideally use a macro to generate it. impl OpaqueImpl for RistrettoTripleDhArgonSuite { fn start_client_registration( - &self, + &mut self, password: &str, ) -> Result { - let result = ClientRegistration::::start(&mut *self.get_rng()?, password.as_bytes())?; + let result = ClientRegistration::::start(self.get_rng(), password.as_bytes())?; Ok(types::ClientRegistrationStartResult { registration_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) } fn start_server_registration( - &self, + &mut self, server_setup: Option<&[u8]>, registration_request: &[u8], username: &str, ) -> Result { let server_setup = match server_setup { Some(server_setup) => ServerSetup::::deserialize(server_setup)?, - None => ServerSetup::::new(&mut *self.get_rng()?), + None => ServerSetup::::new(self.get_rng()), }; let result = ServerRegistration::start( &server_setup, @@ -229,7 +231,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } fn finish_client_registration( - &self, + &mut self, state: &[u8], registration_response: &[u8], password: &str, @@ -237,7 +239,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { let state = ClientRegistration::::deserialize(state)?; let ksf = self.get_ksf()?; let result = state.finish( - &mut *self.get_rng()?, + self.get_rng(), password.as_bytes(), RegistrationResponse::deserialize(registration_response)?, ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)), @@ -249,7 +251,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } fn finish_server_registration( - &self, + &mut self, registration_upload: &[u8], ) -> Result { let registration = ServerRegistration::finish(RegistrationUpload::::deserialize( @@ -260,22 +262,25 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } - fn start_client_login(&self, password: &str) -> Result { - let result = ClientLogin::::start(&mut *self.get_rng()?, password.as_bytes())?; + fn start_client_login( + &mut self, + password: &str, + ) -> Result { + let result = ClientLogin::::start(self.get_rng(), password.as_bytes())?; Ok(types::ClientLoginStartResult { credential_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) } fn start_server_login( - &self, + &mut self, server_setup: &[u8], server_registration: &[u8], credential_request: &[u8], username: &str, ) -> Result { let result = ServerLogin::start( - &mut *self.get_rng()?, + self.get_rng(), &ServerSetup::::deserialize(server_setup)?, Some(ServerRegistration::::deserialize( server_registration, @@ -290,7 +295,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } fn finish_client_login( - &self, + &mut self, state: &[u8], credential_response: &[u8], password: &str, @@ -309,7 +314,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { }) } fn finish_server_login( - &self, + &mut self, state: &[u8], credential_finalization: &[u8], ) -> Result { @@ -326,15 +331,12 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite { pub fn register_seeded_fake_config(seed: [u8; 32]) -> Result<(Vec, Vec), Error> { use rand::RngCore as _; - let config = CipherConfiguration::fake_from_seed(seed); + let mut config = CipherConfiguration::fake_from_seed(seed); let mut password: [u8; 32] = [0; 32]; let mut username: [u8; 32] = [0; 32]; - { - let mut rng = config.rng.borrow_mut(); - rng.fill_bytes(&mut password); - rng.fill_bytes(&mut username); - } + config.rng.fill_bytes(&mut password); + config.rng.fill_bytes(&mut username); let password = hex::encode(password); let username = hex::encode(username); diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs index 281e1529..630c8809 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, str::FromStr}; +use std::str::FromStr; use generic_array::{ArrayLength, GenericArray}; use opaque_ke::errors::InternalError; @@ -53,17 +53,17 @@ pub struct CipherConfiguration { pub ksf: Ksf, #[serde(skip, default = "default_rng")] - pub(crate) rng: RefCell, + pub(crate) rng: ChaCha20Rng, } -fn default_rng() -> RefCell { - RefCell::from(ChaCha20Rng::from_entropy()) +fn default_rng() -> ChaCha20Rng { + ChaCha20Rng::from_entropy() } impl CipherConfiguration { pub(crate) fn fake_from_seed(seed: [u8; 32]) -> Self { Self { - rng: RefCell::from(ChaCha20Rng::from_seed(seed)), + rng: ChaCha20Rng::from_seed(seed), ..Default::default() } } From 11d336a4900bad50a4952137d0e2e70a78415d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 12:12:24 +0100 Subject: [PATCH 72/89] Try to catch panics at the FFI layer if possible --- .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 201 ++++++++++-------- .../Bitwarden.Opaque/rust/src/ffi/types.rs | 1 + extensions/Bitwarden.Opaque/rust/src/lib.rs | 1 + .../src/BitwardenException.cs | 1 + 4 files changed, 118 insertions(+), 86 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index 33a2ee9b..4a2202f1 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -1,4 +1,4 @@ -use std::{ffi::c_char, str::FromStr}; +use std::{ffi::c_char, panic::UnwindSafe, str::FromStr}; use crate::{ Error, @@ -15,17 +15,27 @@ unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a .map_err(|_| Error::InvalidInput(name.into())) } +fn catch(f: impl FnOnce() -> Response + UnwindSafe) -> Response { + match std::panic::catch_unwind(f) { + Ok(r) => r, + Err(e) => Response::error(Error::InternalError(format!("Panic: {e:?}"))), + } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response { - let seed = try_ffi!(unsafe { seed.as_slice() }); - let seed = try_ffi!( - <[u8; 32]>::try_from(seed).map_err(|_| Error::InvalidInput("Seed must be 32 bytes".into())) - ); - - let (server_setup, server_registration) = - try_ffi!(crate::opaque::register_seeded_fake_config(seed)); - - Response::ok2(server_setup, server_registration) + catch(|| { + let seed = try_ffi!(unsafe { seed.as_slice() }); + let seed = try_ffi!( + <[u8; 32]>::try_from(seed) + .map_err(|_| Error::InvalidInput("Seed must be 32 bytes".into())) + ); + + let (server_setup, server_registration) = + try_ffi!(crate::opaque::register_seeded_fake_config(seed)); + + Response::ok2(server_setup, server_registration) + }) } /// @@ -37,14 +47,16 @@ pub unsafe extern "C" fn start_client_registration( config: *const c_char, password: *const c_char, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let password: &str = try_ffi!(unsafe { parse_str(password, "password") }); + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let password: &str = try_ffi!(unsafe { parse_str(password, "password") }); - let mut config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); - let result = try_ffi!(config.start_client_registration(password)); + let result = try_ffi!(config.start_client_registration(password)); - Response::ok2(result.registration_request, result.state) + Response::ok2(result.registration_request, result.state) + }) } /// @@ -58,17 +70,22 @@ pub unsafe extern "C" fn start_server_registration( registration_request: Buffer, username: *const c_char, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let server_setup = unsafe { server_setup.as_slice() }.ok(); - let registration_request = try_ffi!(unsafe { registration_request.as_slice() }); - let username = try_ffi!(unsafe { parse_str(username, "username") }); - - let mut config = try_ffi!(CipherConfiguration::from_str(config)); - - let response = - try_ffi!(config.start_server_registration(server_setup, registration_request, username)); - - Response::ok2(response.registration_response, response.server_setup) + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let server_setup = unsafe { server_setup.as_slice() }.ok(); + let registration_request = try_ffi!(unsafe { registration_request.as_slice() }); + let username = try_ffi!(unsafe { parse_str(username, "username") }); + + let mut config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = try_ffi!(config.start_server_registration( + server_setup, + registration_request, + username + )); + + Response::ok2(response.registration_response, response.server_setup) + }) } /// @@ -82,21 +99,23 @@ pub unsafe extern "C" fn finish_client_registration( registration_response: Buffer, password: *const c_char, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let registration_response = try_ffi!(unsafe { registration_response.as_slice() }); - let state = try_ffi!(unsafe { state.as_slice() }); - let password = try_ffi!(unsafe { parse_str(password, "password") }); - - let mut config = try_ffi!(CipherConfiguration::from_str(config)); - - let response = - try_ffi!(config.finish_client_registration(state, registration_response, password)); - - Response::ok3( - response.registration_upload, - response.export_key, - response.server_s_pk, - ) + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let registration_response = try_ffi!(unsafe { registration_response.as_slice() }); + let state = try_ffi!(unsafe { state.as_slice() }); + let password = try_ffi!(unsafe { parse_str(password, "password") }); + + let mut config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = + try_ffi!(config.finish_client_registration(state, registration_response, password)); + + Response::ok3( + response.registration_upload, + response.export_key, + response.server_s_pk, + ) + }) } /// @@ -108,13 +127,15 @@ pub unsafe extern "C" fn finish_server_registration( config: *const c_char, registration_upload: Buffer, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let registration_upload = try_ffi!(unsafe { registration_upload.as_slice() }); + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let registration_upload = try_ffi!(unsafe { registration_upload.as_slice() }); - let mut config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); - let response = try_ffi!(config.finish_server_registration(registration_upload)); - Response::ok1(response.server_registration) + let response = try_ffi!(config.finish_server_registration(registration_upload)); + Response::ok1(response.server_registration) + }) } /// @@ -126,13 +147,15 @@ pub unsafe extern "C" fn start_client_login( config: *const c_char, password: *const c_char, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let password = try_ffi!(unsafe { parse_str(password, "password") }); + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let password = try_ffi!(unsafe { parse_str(password, "password") }); - let mut config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); - let response = try_ffi!(config.start_client_login(password)); - Response::ok2(response.credential_request, response.state) + let response = try_ffi!(config.start_client_login(password)); + Response::ok2(response.credential_request, response.state) + }) } /// @@ -147,21 +170,23 @@ pub unsafe extern "C" fn start_server_login( credential_request: Buffer, username: *const c_char, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let server_setup = try_ffi!(unsafe { server_setup.as_slice() }); - let server_registration = try_ffi!(unsafe { server_registration.as_slice() }); - let credential_request = try_ffi!(unsafe { credential_request.as_slice() }); - let username = try_ffi!(unsafe { parse_str(username, "username") }); - - let mut config = try_ffi!(CipherConfiguration::from_str(config)); - - let response = try_ffi!(config.start_server_login( - server_setup, - server_registration, - credential_request, - username, - )); - Response::ok2(response.credential_response, response.state) + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let server_setup = try_ffi!(unsafe { server_setup.as_slice() }); + let server_registration = try_ffi!(unsafe { server_registration.as_slice() }); + let credential_request = try_ffi!(unsafe { credential_request.as_slice() }); + let username = try_ffi!(unsafe { parse_str(username, "username") }); + + let mut config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = try_ffi!(config.start_server_login( + server_setup, + server_registration, + credential_request, + username, + )); + Response::ok2(response.credential_response, response.state) + }) } /// @@ -175,20 +200,22 @@ pub unsafe extern "C" fn finish_client_login( credential_response: Buffer, password: *const c_char, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let state = try_ffi!(unsafe { state.as_slice() }); - let credential_response = try_ffi!(unsafe { credential_response.as_slice() }); - let password = try_ffi!(unsafe { parse_str(password, "password") }); - - let mut config = try_ffi!(CipherConfiguration::from_str(config)); - - let response = try_ffi!(config.finish_client_login(state, credential_response, password)); - Response::ok4( - response.credential_finalization, - response.session_key, - response.export_key, - response.server_s_pk, - ) + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let state = try_ffi!(unsafe { state.as_slice() }); + let credential_response = try_ffi!(unsafe { credential_response.as_slice() }); + let password = try_ffi!(unsafe { parse_str(password, "password") }); + + let mut config = try_ffi!(CipherConfiguration::from_str(config)); + + let response = try_ffi!(config.finish_client_login(state, credential_response, password)); + Response::ok4( + response.credential_finalization, + response.session_key, + response.export_key, + response.server_s_pk, + ) + }) } /// @@ -201,13 +228,15 @@ pub unsafe extern "C" fn finish_server_login( state: Buffer, credential_finalization: Buffer, ) -> Response { - let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let state = try_ffi!(unsafe { state.as_slice() }); - let credential_finalization = try_ffi!(unsafe { credential_finalization.as_slice() }); + catch(|| { + let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); + let state = try_ffi!(unsafe { state.as_slice() }); + let credential_finalization = try_ffi!(unsafe { credential_finalization.as_slice() }); - let mut config = try_ffi!(CipherConfiguration::from_str(config)); + let mut config = try_ffi!(CipherConfiguration::from_str(config)); - let response = try_ffi!(config.finish_server_login(state, credential_finalization)); + let response = try_ffi!(config.finish_server_login(state, credential_finalization)); - Response::ok1(response.session_key) + Response::ok1(response.session_key) + }) } diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs index 94be60ee..cbbdec58 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs @@ -131,6 +131,7 @@ impl Response { Error::InvalidInput(name) => (1, name), Error::InvalidConfig(error) => (2, error), Error::Protocol(e) => (3, format!("{:?}", e)), + Error::InternalError(error) => (4, error), }; Response { diff --git a/extensions/Bitwarden.Opaque/rust/src/lib.rs b/extensions/Bitwarden.Opaque/rust/src/lib.rs index dac6be27..987a3c41 100644 --- a/extensions/Bitwarden.Opaque/rust/src/lib.rs +++ b/extensions/Bitwarden.Opaque/rust/src/lib.rs @@ -13,6 +13,7 @@ pub enum Error { InvalidInput(String), InvalidConfig(String), Protocol(opaque_ke::errors::ProtocolError), + InternalError(String), } impl From for Error { diff --git a/extensions/Bitwarden.Opaque/src/BitwardenException.cs b/extensions/Bitwarden.Opaque/src/BitwardenException.cs index 3ddf0292..bcd2f051 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenException.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenException.cs @@ -16,6 +16,7 @@ private static string getCodeName(int code) 1 => "INVALID_INPUT", 2 => "INVALID_CONFIG", 3 => "PROTOCOL_ERROR", + 4 => "INTERNAL_ERRROR", // This is a special case and it's only used in the C# code. 100 => "UNEXPECTED_RETURN", From 69adec5c477ae22bdabea1b6fbb27da40f605d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 12:14:39 +0100 Subject: [PATCH 73/89] Use opaque_ke Identity instead --- extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs | 6 +++--- .../Bitwarden.Opaque/rust/src/opaque/types.rs | 14 -------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index 49c82feb..0ba1bc7f 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -170,10 +170,10 @@ impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; // this is run on only the client side anyways. Using an identity here means the test vectors do not match when using client functions // from this binding. - type Ksf = IdentityKsf; + type Ksf = opaque_ke::ksf::Identity; } impl OpaqueUtil for RistrettoTripleDhArgonSuite { - type Output = IdentityKsf; + type Output = opaque_ke::ksf::Identity; fn as_variant(config: &CipherConfiguration) -> Option { match config { @@ -189,7 +189,7 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { } } fn get_ksf(&self) -> Result { - Ok(IdentityKsf {}) + Ok(opaque_ke::ksf::Identity) } fn get_rng(&mut self) -> &mut ChaCha20Rng { diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs index 630c8809..f406b160 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs @@ -1,7 +1,5 @@ use std::str::FromStr; -use generic_array::{ArrayLength, GenericArray}; -use opaque_ke::errors::InternalError; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use serde::{Deserialize, Serialize}; @@ -139,15 +137,3 @@ pub(crate) struct ClientLoginFinishResult { pub(crate) struct ServerLoginFinishResult { pub(crate) session_key: Vec, } - -#[derive(Debug, Default)] -pub(crate) struct IdentityKsf {} - -impl opaque_ke::ksf::Ksf for IdentityKsf { - fn hash>( - &self, - input: GenericArray, - ) -> Result, InternalError> { - Ok(input) - } -} From 41dc8229fe9e5b96d02ff02e46e4bccfa7a9ef26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 12:22:54 +0100 Subject: [PATCH 74/89] Avoid cloning Rng --- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index 0ba1bc7f..7bb6dccd 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -57,9 +57,9 @@ pub trait OpaqueImpl { // This trait exists to extract the differences between all the OpaqueImpl implementations. // This would allow replacing those impls by a macro in the future. -pub trait OpaqueUtil: Sized { +pub trait OpaqueUtil<'a>: Sized { type Output; - fn as_variant(config: &CipherConfiguration) -> Option; + fn as_variant(config: &'a mut CipherConfiguration) -> Option; fn get_ksf(&self) -> Result; fn get_rng(&mut self) -> &mut ChaCha20Rng; } @@ -163,8 +163,8 @@ impl OpaqueImpl for CipherConfiguration { } // Define the cipher suite and implement the required traits on it (opaque_ke::CipherSuite+OpaqueUtil+OpaqueImpl) -struct RistrettoTripleDhArgonSuite(ChaCha20Rng); -impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { +struct RistrettoTripleDhArgonSuite<'a>(&'a mut ChaCha20Rng); +impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite<'_> { type OprfCs = opaque_ke::Ristretto255; type KeGroup = opaque_ke::Ristretto255; type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; @@ -172,10 +172,10 @@ impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite { // from this binding. type Ksf = opaque_ke::ksf::Identity; } -impl OpaqueUtil for RistrettoTripleDhArgonSuite { +impl<'a> OpaqueUtil<'a> for RistrettoTripleDhArgonSuite<'a> { type Output = opaque_ke::ksf::Identity; - fn as_variant(config: &CipherConfiguration) -> Option { + fn as_variant(config: &'a mut CipherConfiguration) -> Option { match config { CipherConfiguration { opaque_version: 3, @@ -184,7 +184,7 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { key_exchange: KeyExchange::TripleDh, ksf: _, rng, - } => Some(Self(rng.clone())), + } => Some(Self(rng)), _ => None, } } @@ -193,13 +193,13 @@ impl OpaqueUtil for RistrettoTripleDhArgonSuite { } fn get_rng(&mut self) -> &mut ChaCha20Rng { - &mut self.0 + self.0 } } // This implementation will be identical between any cipher suite, but we can't simply reuse it because of all the generic bounds on the CipherSuite trait. // If we need to add more cipher suites, we will need to copy this implementation over, or ideally use a macro to generate it. -impl OpaqueImpl for RistrettoTripleDhArgonSuite { +impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { fn start_client_registration( &mut self, password: &str, From 95c58089a892f9e85f971a000ac1cebd0cd822c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 14:17:26 +0100 Subject: [PATCH 75/89] Implement dyn dispatch for Argon and identity --- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 537 +++++++++--------- .../Bitwarden.Opaque/rust/src/opaque/types.rs | 3 + 2 files changed, 263 insertions(+), 277 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index 7bb6dccd..f9a5ecdf 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -7,6 +7,122 @@ mod types; pub(crate) use types::*; +// This trait exists to extract the differences between all the OpaqueImpl implementations. +pub trait OpaqueUtil<'a>: opaque_ke::CipherSuite + OpaqueImpl + Sized { + fn as_variant(config: &'a mut CipherConfiguration) -> Option; + fn get_ksf(&self) -> Result; + fn get_rng(&mut self) -> &mut ChaCha20Rng; +} + +fn invalid_config(config: &CipherConfiguration) -> Error { + Error::InvalidConfig(serde_json::to_string(config).unwrap_or_default()) +} + +// Define the cipher suites and implement the required traits on them (opaque_ke::CipherSuite+OpaqueUtil) +struct RistrettoTripleDhArgonSuite<'a>(&'a mut ChaCha20Rng, Argon2id); +impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite<'_> { + type OprfCs = opaque_ke::Ristretto255; + type KeGroup = opaque_ke::Ristretto255; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; + type Ksf = argon2::Argon2<'static>; +} +impl<'a> OpaqueUtil<'a> for RistrettoTripleDhArgonSuite<'a> { + fn as_variant(config: &'a mut CipherConfiguration) -> Option { + match config { + CipherConfiguration { + opaque_version: 3, + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: types::Ksf::Argon2id(argon), + rng, + } => Some(Self(rng, *argon)), + _ => None, + } + } + fn get_ksf(&self) -> Result { + Ok(argon2::Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new(self.1.memory, self.1.iterations, self.1.parallelism, None) + .map_err(|_| Error::InvalidConfig("Invalid Argon2 parameters".into()))?, + )) + } + + fn get_rng(&mut self) -> &mut ChaCha20Rng { + self.0 + } +} + +struct RistrettoTripleDhIdentitySuite<'a>(&'a mut ChaCha20Rng); +impl opaque_ke::CipherSuite for RistrettoTripleDhIdentitySuite<'_> { + type OprfCs = opaque_ke::Ristretto255; + type KeGroup = opaque_ke::Ristretto255; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; + type Ksf = ksf::Identity; +} +impl<'a> OpaqueUtil<'a> for RistrettoTripleDhIdentitySuite<'a> { + fn as_variant(config: &'a mut CipherConfiguration) -> Option { + match config { + CipherConfiguration { + opaque_version: 3, + oprf_cs: OprfCs::Ristretto255, + ke_group: KeGroup::Ristretto255, + key_exchange: KeyExchange::TripleDh, + ksf: types::Ksf::Identity, + rng, + } => Some(Self(rng)), + _ => None, + } + } + fn get_ksf(&self) -> Result { + Ok(ksf::Identity) + } + + fn get_rng(&mut self) -> &mut ChaCha20Rng { + self.0 + } +} + +pub fn register_seeded_fake_config(seed: [u8; 32]) -> Result<(Vec, Vec), Error> { + use rand::RngCore as _; + + let mut config = CipherConfiguration::fake_from_seed(seed); + + let mut password: [u8; 32] = [0; 32]; + let mut username: [u8; 32] = [0; 32]; + config.rng.fill_bytes(&mut password); + config.rng.fill_bytes(&mut username); + let password = hex::encode(password); + let username = hex::encode(username); + + let start = config.start_client_registration(password.as_str())?; + let server_start = + config.start_server_registration(None, &start.registration_request, &username)?; + let client_finish = config.finish_client_registration( + &start.state, + &server_start.registration_response, + &password, + )?; + let server_finish = config.finish_server_registration(&client_finish.registration_upload)?; + Ok((server_start.server_setup, server_finish.server_registration)) +} + +// This generic utility function is used to dynamically dispatch to the correct cipher suite +fn with_variants( + config: &mut CipherConfiguration, + func: impl FnOnce(&mut dyn OpaqueImpl) -> Result, +) -> Result { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(config) { + return func(&mut suite); + }; + + if let Some(mut suite) = RistrettoTripleDhIdentitySuite::as_variant(config) { + return func(&mut suite); + }; + Err(invalid_config(config)) +} + // The opaque-ke crate uses a lot of generic traits, which are difficult to handle in FFI. // This trait implements dynamic dispatch to allow using opaque-ke without generics. pub trait OpaqueImpl { @@ -55,31 +171,13 @@ pub trait OpaqueImpl { ) -> Result; } -// This trait exists to extract the differences between all the OpaqueImpl implementations. -// This would allow replacing those impls by a macro in the future. -pub trait OpaqueUtil<'a>: Sized { - type Output; - fn as_variant(config: &'a mut CipherConfiguration) -> Option; - fn get_ksf(&self) -> Result; - fn get_rng(&mut self) -> &mut ChaCha20Rng; -} - -fn invalid_config(config: &CipherConfiguration) -> Error { - Error::InvalidConfig(serde_json::to_string(config).unwrap_or_default()) -} - -// Implement the OpaqueImpl trait for the CipherConfiguration enum, which allows us to dynamically dispatch to the correct cipher suite. +// Implement OpaqueImpl for the shared type, and dynamically dispatch to the correct cipher suite impl OpaqueImpl for CipherConfiguration { fn start_client_registration( &mut self, password: &str, ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.start_client_registration(password); - }; - Err(Error::InvalidConfig( - serde_json::to_string(self).unwrap_or_default(), - )) + with_variants(self, |suite| suite.start_client_registration(password)) } fn start_server_registration( &mut self, @@ -87,147 +185,8 @@ impl OpaqueImpl for CipherConfiguration { registration_request: &[u8], username: &str, ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.start_server_registration(server_setup, registration_request, username); - }; - Err(invalid_config(self)) - } - fn finish_client_registration( - &mut self, - state: &[u8], - registration_response: &[u8], - password: &str, - ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.finish_client_registration(state, registration_response, password); - }; - Err(invalid_config(self)) - } - fn finish_server_registration( - &mut self, - registration_upload: &[u8], - ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.finish_server_registration(registration_upload); - }; - Err(invalid_config(self)) - } - - fn start_client_login( - &mut self, - password: &str, - ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.start_client_login(password); - }; - Err(invalid_config(self)) - } - fn start_server_login( - &mut self, - server_setup: &[u8], - server_registration: &[u8], - credential_request: &[u8], - username: &str, - ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.start_server_login( - server_setup, - server_registration, - credential_request, - username, - ); - }; - Err(invalid_config(self)) - } - fn finish_client_login( - &mut self, - state: &[u8], - credential_response: &[u8], - password: &str, - ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.finish_client_login(state, credential_response, password); - }; - Err(invalid_config(self)) - } - fn finish_server_login( - &mut self, - state: &[u8], - credential_finalization: &[u8], - ) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(self) { - return suite.finish_server_login(state, credential_finalization); - }; - Err(invalid_config(self)) - } -} - -// Define the cipher suite and implement the required traits on it (opaque_ke::CipherSuite+OpaqueUtil+OpaqueImpl) -struct RistrettoTripleDhArgonSuite<'a>(&'a mut ChaCha20Rng); -impl opaque_ke::CipherSuite for RistrettoTripleDhArgonSuite<'_> { - type OprfCs = opaque_ke::Ristretto255; - type KeGroup = opaque_ke::Ristretto255; - type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDh; - // this is run on only the client side anyways. Using an identity here means the test vectors do not match when using client functions - // from this binding. - type Ksf = opaque_ke::ksf::Identity; -} -impl<'a> OpaqueUtil<'a> for RistrettoTripleDhArgonSuite<'a> { - type Output = opaque_ke::ksf::Identity; - - fn as_variant(config: &'a mut CipherConfiguration) -> Option { - match config { - CipherConfiguration { - opaque_version: 3, - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: _, - rng, - } => Some(Self(rng)), - _ => None, - } - } - fn get_ksf(&self) -> Result { - Ok(opaque_ke::ksf::Identity) - } - - fn get_rng(&mut self) -> &mut ChaCha20Rng { - self.0 - } -} - -// This implementation will be identical between any cipher suite, but we can't simply reuse it because of all the generic bounds on the CipherSuite trait. -// If we need to add more cipher suites, we will need to copy this implementation over, or ideally use a macro to generate it. -impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { - fn start_client_registration( - &mut self, - password: &str, - ) -> Result { - let result = ClientRegistration::::start(self.get_rng(), password.as_bytes())?; - Ok(types::ClientRegistrationStartResult { - registration_request: result.message.serialize().to_vec(), - state: result.state.serialize().to_vec(), - }) - } - fn start_server_registration( - &mut self, - server_setup: Option<&[u8]>, - registration_request: &[u8], - username: &str, - ) -> Result { - let server_setup = match server_setup { - Some(server_setup) => ServerSetup::::deserialize(server_setup)?, - None => ServerSetup::::new(self.get_rng()), - }; - let result = ServerRegistration::start( - &server_setup, - RegistrationRequest::deserialize(registration_request)?, - username.as_bytes(), - )?; - Ok(types::ServerRegistrationStartResult { - registration_response: result.message.serialize().to_vec(), - server_setup: server_setup.serialize().to_vec(), + with_variants(self, |suite| { + suite.start_server_registration(server_setup, registration_request, username) }) } fn finish_client_registration( @@ -236,29 +195,16 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { registration_response: &[u8], password: &str, ) -> Result { - let state = ClientRegistration::::deserialize(state)?; - let ksf = self.get_ksf()?; - let result = state.finish( - self.get_rng(), - password.as_bytes(), - RegistrationResponse::deserialize(registration_response)?, - ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)), - )?; - Ok(types::ClientRegistrationFinishResult { - registration_upload: result.message.serialize().to_vec(), - export_key: result.export_key.to_vec(), - server_s_pk: result.server_s_pk.serialize().to_vec(), + with_variants(self, |suite| { + suite.finish_client_registration(state, registration_response, password) }) } fn finish_server_registration( &mut self, registration_upload: &[u8], ) -> Result { - let registration = ServerRegistration::finish(RegistrationUpload::::deserialize( - registration_upload, - )?); - Ok(types::ServerRegistrationFinishResult { - server_registration: registration.serialize().to_vec(), + with_variants(self, |suite| { + suite.finish_server_registration(registration_upload) }) } @@ -266,11 +212,7 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { &mut self, password: &str, ) -> Result { - let result = ClientLogin::::start(self.get_rng(), password.as_bytes())?; - Ok(types::ClientLoginStartResult { - credential_request: result.message.serialize().to_vec(), - state: result.state.serialize().to_vec(), - }) + with_variants(self, |suite| suite.start_client_login(password)) } fn start_server_login( &mut self, @@ -279,19 +221,13 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { credential_request: &[u8], username: &str, ) -> Result { - let result = ServerLogin::start( - self.get_rng(), - &ServerSetup::::deserialize(server_setup)?, - Some(ServerRegistration::::deserialize( + with_variants(self, |suite| { + suite.start_server_login( + server_setup, server_registration, - )?), - CredentialRequest::deserialize(credential_request)?, - username.as_bytes(), - ServerLoginStartParameters::default(), - )?; - Ok(types::ServerLoginStartResult { - credential_response: result.message.serialize().to_vec(), - state: result.state.serialize().to_vec(), + credential_request, + username, + ) }) } fn finish_client_login( @@ -300,17 +236,8 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { credential_response: &[u8], password: &str, ) -> Result { - let client_login = ClientLogin::::deserialize(state)?; - let result = client_login.finish( - password.as_bytes(), - CredentialResponse::deserialize(credential_response)?, - ClientLoginFinishParameters::new(None, Identifiers::default(), Some(&self.get_ksf()?)), - )?; - Ok(types::ClientLoginFinishResult { - credential_finalization: result.message.serialize().to_vec(), - session_key: result.session_key.to_vec(), - export_key: result.export_key.to_vec(), - server_s_pk: result.server_s_pk.serialize().to_vec(), + with_variants(self, |suite| { + suite.finish_client_login(state, credential_response, password) }) } fn finish_server_login( @@ -318,84 +245,140 @@ impl OpaqueImpl for RistrettoTripleDhArgonSuite<'_> { state: &[u8], credential_finalization: &[u8], ) -> Result { - let server_login = ServerLogin::::deserialize(state)?; - let result = server_login.finish(CredentialFinalization::deserialize( - credential_finalization, - )?)?; - Ok(types::ServerLoginFinishResult { - session_key: result.session_key.to_vec(), + with_variants(self, |suite| { + suite.finish_server_login(state, credential_finalization) }) } } -pub fn register_seeded_fake_config(seed: [u8; 32]) -> Result<(Vec, Vec), Error> { - use rand::RngCore as _; - - let mut config = CipherConfiguration::fake_from_seed(seed); +macro_rules! implement_cipher_suites { + ( $( $name:ident ),+ $(,)? ) => { + // Check that any type implements the required trait + const fn _assert_impl_traits<'a, T: OpaqueUtil<'a>>() {} + const _: () = { $( _assert_impl_traits::<$name>(); )+ }; - let mut password: [u8; 32] = [0; 32]; - let mut username: [u8; 32] = [0; 32]; - config.rng.fill_bytes(&mut password); - config.rng.fill_bytes(&mut username); - let password = hex::encode(password); - let username = hex::encode(username); - - let start = config.start_client_registration(password.as_str())?; - let server_start = - config.start_server_registration(None, &start.registration_request, &username)?; - let client_finish = config.finish_client_registration( - &start.state, - &server_start.registration_response, - &password, - )?; - let server_finish = config.finish_server_registration(&client_finish.registration_upload)?; - Ok((server_start.server_setup, server_finish.server_registration)) -} + // Implement OpaqueImpl for each cipher suite. + // This is just copying the implementation on RistrettoTripleDhArgonSuite and wrapping it in $()+ + $(impl OpaqueImpl for $name<'_> { + fn start_client_registration( + &mut self, + password: &str, + ) -> Result { + let result = ClientRegistration::::start(self.get_rng(), password.as_bytes())?; + Ok(types::ClientRegistrationStartResult { + registration_request: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) + } + fn start_server_registration( + &mut self, + server_setup: Option<&[u8]>, + registration_request: &[u8], + username: &str, + ) -> Result { + let server_setup = match server_setup { + Some(server_setup) => ServerSetup::::deserialize(server_setup)?, + None => ServerSetup::::new(self.get_rng()), + }; + let result = ServerRegistration::start(&server_setup, RegistrationRequest::deserialize(registration_request)?, username.as_bytes())?; + Ok(types::ServerRegistrationStartResult { + registration_response: result.message.serialize().to_vec(), + server_setup: server_setup.serialize().to_vec(), + }) + } + fn finish_client_registration( + &mut self, + state: &[u8], + registration_response: &[u8], + password: &str, + ) -> Result { + let state = ClientRegistration::::deserialize(state)?; + let ksf = self.get_ksf()?; + let response = RegistrationResponse::deserialize(registration_response)?; + let params = ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)); + let result = state.finish(self.get_rng(), password.as_bytes(), response, params)?; + Ok(types::ClientRegistrationFinishResult { + registration_upload: result.message.serialize().to_vec(), + export_key: result.export_key.to_vec(), + server_s_pk: result.server_s_pk.serialize().to_vec(), + }) + } + fn finish_server_registration( + &mut self, + registration_upload: &[u8], + ) -> Result { + let upload = RegistrationUpload::::deserialize(registration_upload)?; + let registration = ServerRegistration::finish(upload); + Ok(types::ServerRegistrationFinishResult { + server_registration: registration.serialize().to_vec(), + }) + } -/* - TODO: If in the future we add more cipher suites, we will need to create - a new OpaqueImpl implementation and add it to the CipherConfiguration matches. - This will require a lot of duplication, but can be simplified with a macro as follows: + fn start_client_login( + &mut self, + password: &str, + ) -> Result { + let result = ClientLogin::::start(self.get_rng(), password.as_bytes())?; + Ok(types::ClientLoginStartResult { + credential_request: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) + } + fn start_server_login( + &mut self, + server_setup: &[u8], + server_registration: &[u8], + credential_request: &[u8], + username: &str, + ) -> Result { - macro_rules! implement_cipher_suites { - ( $shared_type:ident; $( $name:ident : $pat:pat => $cipher:expr );+ ) => { - // Check that any type implements the required traits - const fn _assert_opaque_ksf_and_cipher_suite() {} - const _: () = { $( _assert_opaque_ksf_and_cipher_suite::<$name>(); )+ }; + let server_setup = ServerSetup::::deserialize(server_setup)?; + let server_registration = ServerRegistration::::deserialize(server_registration)?; + let credential_request = CredentialRequest::deserialize(credential_request)?; - // Implement OpaqueImpl for the shared type, and dispatch to the correct cipher suite - impl OpaqueImpl for $shared_type { - fn start_client_registration(&self, password: &str) -> Result { - $(if let Some(suite) = $name::as_variant(self) { - return suite.start_client_registration(password); - };)+ - Err(Error::InvalidInput("Invalid cipher configuration")) - } - ... + let result = ServerLogin::start( + self.get_rng(), + &server_setup, + Some(server_registration), + credential_request, + username.as_bytes(), + ServerLoginStartParameters::default(), + )?; + Ok(types::ServerLoginStartResult { + credential_response: result.message.serialize().to_vec(), + state: result.state.serialize().to_vec(), + }) } + fn finish_client_login( + &mut self, + state: &[u8], + credential_response: &[u8], + password: &str, + ) -> Result { + let client_login = ClientLogin::::deserialize(state)?; + let ksf = self.get_ksf()?; + let params = ClientLoginFinishParameters::new(None, Identifiers::default(), Some(&ksf)); + let result = client_login.finish(password.as_bytes(), CredentialResponse::deserialize(credential_response)?, params)?; + Ok(types::ClientLoginFinishResult { + credential_finalization: result.message.serialize().to_vec(), + session_key: result.session_key.to_vec(), + export_key: result.export_key.to_vec(), + server_s_pk: result.server_s_pk.serialize().to_vec(), + }) + } + fn finish_server_login( + &mut self, + state: &[u8], + credential_finalization: &[u8], + ) -> Result { + let server_login = ServerLogin::::deserialize(state)?; + let result = server_login.finish(CredentialFinalization::deserialize(credential_finalization)?)?; + Ok(types::ServerLoginFinishResult { + session_key: result.session_key.to_vec(), + }) + } + })+ + }; +} - // Implement OpaqueImpl for each cipher suite. - // This is just copying the implementation on RistrettoTripleDhArgonSuite and wrapping it in $()+ - $(impl OpaqueImpl for $name { - fn start_client_registration(&self, password: &str) -> Result { - let result = ClientRegistration::::start(&mut OsRng, password.as_bytes())?; - Ok(types::ClientRegistrationStartResult { - registration_request: result.message.serialize().to_vec(), - state: result.state.serialize().to_vec(), - }) - } - ... - })+ - }; - } - - implement_cipher_suites! { - CipherConfiguration; - RistrettoTripleDhArgonSuite: CipherConfiguration { - oprf_cs: OprfCs::Ristretto255, - ke_group: KeGroup::Ristretto255, - key_exchange: KeyExchange::TripleDh, - ksf: Ksf::Argon2id(argon), - } => RistrettoTripleDhArgonSuite(*argon) - } -*/ +implement_cipher_suites!(RistrettoTripleDhArgonSuite, RistrettoTripleDhIdentitySuite); diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs index f406b160..b842b308 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/types.rs @@ -29,6 +29,8 @@ pub enum KeyExchange { #[serde(rename_all = "camelCase", tag = "algorithm", content = "parameters")] pub enum Ksf { Argon2id(Argon2id), + #[serde(skip)] + Identity, __NonExhaustive(()), } @@ -61,6 +63,7 @@ fn default_rng() -> ChaCha20Rng { impl CipherConfiguration { pub(crate) fn fake_from_seed(seed: [u8; 32]) -> Self { Self { + ksf: Ksf::Identity, rng: ChaCha20Rng::from_seed(seed), ..Default::default() } From 72c5fe51584090ac59fc46834e13cdda9edee9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 15:07:25 +0100 Subject: [PATCH 76/89] Update safety checks and comments --- .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 52 +++++----- .../Bitwarden.Opaque/rust/src/ffi/types.rs | 43 +++++++- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 1 - .../Bitwarden.Opaque/src/BitwardenLibrary.cs | 99 +++++++++++-------- 4 files changed, 120 insertions(+), 75 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index 4a2202f1..bae6ab91 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -10,8 +10,16 @@ mod types; use types::*; +/// # Safety +/// All the limitations of [std::ffi::CStr::from_ptr] apply, mainly: +/// - The pointer must be valid and point to a null-terminated byte string. +/// - The memory must be valid for the duration of the call and not modified by other threads. unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a str, Error> { - unsafe { std::ffi::CStr::from_ptr(input).to_str() } + if input.is_null() { + return Err(Error::InvalidInput("Input string is null".into())); + } + unsafe { std::ffi::CStr::from_ptr(input) } + .to_str() .map_err(|_| Error::InvalidInput(name.into())) } @@ -38,10 +46,9 @@ pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response { }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_client_registration( config: *const c_char, @@ -59,10 +66,9 @@ pub unsafe extern "C" fn start_client_registration( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_registration( config: *const c_char, @@ -72,7 +78,7 @@ pub unsafe extern "C" fn start_server_registration( ) -> Response { catch(|| { let config: &str = try_ffi!(unsafe { parse_str(config, "config") }); - let server_setup = unsafe { server_setup.as_slice() }.ok(); + let server_setup = try_ffi!(unsafe { server_setup.as_slice_optional() }); let registration_request = try_ffi!(unsafe { registration_request.as_slice() }); let username = try_ffi!(unsafe { parse_str(username, "username") }); @@ -88,10 +94,9 @@ pub unsafe extern "C" fn start_server_registration( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_registration( config: *const c_char, @@ -118,10 +123,9 @@ pub unsafe extern "C" fn finish_client_registration( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_server_registration( config: *const c_char, @@ -138,10 +142,9 @@ pub unsafe extern "C" fn finish_server_registration( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_client_login( config: *const c_char, @@ -158,10 +161,9 @@ pub unsafe extern "C" fn start_client_login( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_login( config: *const c_char, @@ -189,10 +191,9 @@ pub unsafe extern "C" fn start_server_login( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_login( config: *const c_char, @@ -218,10 +219,9 @@ pub unsafe extern "C" fn finish_client_login( }) } -/// -/// /// # Safety -/// ABC +/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_server_login( config: *const c_char, diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs index cbbdec58..f699e8fe 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs @@ -3,10 +3,15 @@ use crate::Error; /// Free the buffer memory. /// /// # Safety -/// The parameter should contain a Buffer pointing to valid initialized memory, or null. +/// This function should ONLY be used with [Buffer]s initialized from the Rust side. +/// Calling it with a [Buffer] initialized from the C# side is undefined behavior. +/// The caller is responsible for ensuring that the memory is not used after this call. #[unsafe(no_mangle)] pub unsafe extern "C" fn free_buffer(buf: Buffer) { - unsafe { buf.free() }; + std::panic::catch_unwind(|| { + unsafe { buf.free() }; + }) + .ok(); } #[repr(C)] @@ -36,13 +41,41 @@ impl Buffer { Buffer { data, len } } - pub unsafe fn as_slice(&self) -> Result<&[u8], Error> { + /// # Safety + /// All the limitations of [std::slice::from_raw_parts] apply, mainly: + /// - The pointer must be either null or valid for reads up to [Buffer::len] bytes. + /// - The memory must be valid for the duration of the call and not modified by other threads. + pub unsafe fn as_slice_optional(&self) -> Result, Error> { if self.data.is_null() { - return Err(Error::InvalidInput("Buffer data is null".into())); + return Ok(None); + } + if self.len >= isize::MAX as usize { + return Err(Error::InvalidInput("Buffer length is too large".into())); + } + if (self.data.addr()) + .overflowing_add_signed(self.len as isize) + .1 + { + return Err(Error::InvalidInput("Buffer data overflows".into())); } - Ok(unsafe { std::slice::from_raw_parts(self.data, self.len) }) + Ok(Some(unsafe { + std::slice::from_raw_parts(self.data, self.len) + })) + } + + /// # Safety + /// All the limitations of [std::slice::from_raw_parts] apply, mainly: + /// - The pointer must be either null or valid for reads up to [Buffer::len] bytes. + /// - The memory must be valid for the duration of the call and not modified by other threads. + pub unsafe fn as_slice(&self) -> Result<&[u8], Error> { + unsafe { self.as_slice_optional() }? + .ok_or_else(|| Error::InvalidInput("Buffer data is null".into())) } + /// # Safety + /// This function should ONLY be used with [Buffer]s initialized from the Rust side. + /// Calling it with a [Buffer] initialized from the C# side is undefined behavior. + /// The caller is responsible for ensuring that the memory is not used after this call. pub unsafe fn free(self) { if !self.data.is_null() { let _ = unsafe { Vec::from_raw_parts(self.data, self.len, self.len) }; diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index f9a5ecdf..661b7c72 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -116,7 +116,6 @@ fn with_variants( if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(config) { return func(&mut suite); }; - if let Some(mut suite) = RistrettoTripleDhIdentitySuite::as_variant(config) { return func(&mut suite); }; diff --git a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs index a57178c3..d8d34135 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs @@ -37,9 +37,9 @@ internal struct Response public Buffer data4; // Utility function to get all buffers as a list - public List GetAllBuffers() + public readonly List GetAllBuffers() { - return new List { data1, data2, data3, data4 }; + return [data1, data2, data3, data4]; } } @@ -76,18 +76,24 @@ public List GetAllBuffers() internal class FFIHandler { + private FFIHandler() { } + private static readonly JsonSerializerOptions _serializerOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, // Converts enums to strings PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - private List _handles = new List(); + private readonly List _handles = []; - internal void FreeHandles() + private void FreeHandles() { foreach (var handle in _handles) handle.Free(); _handles.Clear(); } + /// Create an FFI buffer from the provided byte array. + /// The buffer will be freed when the FFIHandler calls FreeHandles. + /// Important: You should never use free_buffer on the buffer returned by this function, + /// as it is only to be used with Rust allocated buffers. Doing otherwise is undefined behavior. public Buffer Buf(byte[]? data) { var handle = GCHandle.Alloc(data, GCHandleType.Pinned); @@ -98,60 +104,67 @@ public Buffer Buf(byte[]? data) size = data?.Length ?? 0 }; } + /// Serialize the provided configuration to a FFI string. public string Cfg(CipherConfiguration config) { return JsonSerializer.Serialize(config, _serializerOptions); } - } - private static byte[]? CopyAndFreeBuffer(Buffer buffer) - { - if (buffer.data == IntPtr.Zero) return null; - if (buffer.size == 0) return []; - var data = new byte[buffer.size]; - Marshal.Copy(buffer.data, data, 0, (int)buffer.size); - free_buffer(buffer); - return data; - } - - internal static List ExecuteFFIFunction(Func function, int expectedValues) - { - var ffi = new FFIHandler(); - try + internal static List ExecuteFFIFunction(Func function, int expectedValues) { - // Execute the function and get the response - var response = function(ffi); - // If we receive an error, parse the message and throw an exception - if (response.error != 0) + static byte[]? CopyAndFreeBuffer(Buffer buffer) { - var message = CopyAndFreeBuffer(response.error_message); - string messageStr; - try { messageStr = Encoding.UTF8.GetString(message!); } catch { messageStr = ""; } - throw new BitwardenException((int)response.error, messageStr); + if (buffer.data == IntPtr.Zero) return null; + if (buffer.size == 0) return []; + + var data = new byte[buffer.size]; + Marshal.Copy(buffer.data, data, 0, (int)buffer.size); + free_buffer(buffer); + return data; } - // If we don't receive an error, parse all the return types - var arrays = new List { }; - foreach (var buffer in response.GetAllBuffers()) + + var ffi = new FFIHandler(); + try { - var data = CopyAndFreeBuffer(buffer); - if (data == null) break; - arrays.Add(data); + // Execute the function and get the response + var response = function(ffi); + + // If we receive an error, parse the message and throw an exception + if (response.error != 0) + { + var message = CopyAndFreeBuffer(response.error_message); + string messageStr; + try { messageStr = Encoding.UTF8.GetString(message!); } catch { messageStr = ""; } + throw new BitwardenException((int)response.error, messageStr); + } + + // If we don't receive an error, parse all the return types + var arrays = new List { }; + foreach (var buffer in response.GetAllBuffers()) + { + var data = CopyAndFreeBuffer(buffer); + if (data == null) break; + arrays.Add(data); + } + + // If we receive a different number of return values than expected, something must have gone wrong, throw an exception + if (arrays.Count != expectedValues) + { + throw new BitwardenException(100, $"Invalid number of return values. Expected {expectedValues}, got {arrays.Count}"); + } + + return arrays; } - - // If we receive a different number of return values than expected, something must have gone wrong, throw an exception - if (arrays.Count != expectedValues) + finally { - throw new BitwardenException(100, $"Invalid number of return values. Expected {expectedValues}, got {arrays.Count}"); + ffi.FreeHandles(); } - - return arrays; - } - finally - { - ffi.FreeHandles(); } } + + // Just a wrapper to simplify calling + internal static List ExecuteFFIFunction(Func function, int expectedValues) => FFIHandler.ExecuteFFIFunction(function, expectedValues); } From 001d441784de7a15192646a785fcceaaded3c7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 15:15:26 +0100 Subject: [PATCH 77/89] Missed doc --- extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs index 0df816a7..2c3221ac 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs @@ -118,7 +118,10 @@ public ServerLoginFinishResult FinishLogin(CipherConfiguration config, byte[] st sessionKey = result[0] }; } - + /// + /// Generate a seeded fake registration. This can be returned for unenrolled users to avoid account enumeration issues. + /// + /// The seed to use for the fake registration. This should be consistent between multiple calls to the same user public (byte[] serverSetup, byte[] serverRegistration) SeededFakeRegistration(byte[] seed) { var result = BitwardenLibrary.ExecuteFFIFunction((ffi) => From 4094158b8b583abdf16cb7308235b7fe688028f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 15:42:32 +0100 Subject: [PATCH 78/89] Remove unused deps and force unwind --- extensions/Bitwarden.Opaque/rust/Cargo.lock | 3 --- extensions/Bitwarden.Opaque/rust/Cargo.toml | 12 ++++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.lock b/extensions/Bitwarden.Opaque/rust/Cargo.lock index 3fea5e1e..2a5f335a 100644 --- a/extensions/Bitwarden.Opaque/rust/Cargo.lock +++ b/extensions/Bitwarden.Opaque/rust/Cargo.lock @@ -325,15 +325,12 @@ name = "opaque-ke-binding" version = "0.0.0" dependencies = [ "argon2", - "digest", - "generic-array", "hex", "opaque-ke", "rand", "rand_chacha", "serde", "serde_json", - "voprf", "zeroizing-alloc", ] diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.toml b/extensions/Bitwarden.Opaque/rust/Cargo.toml index 14391d79..1e6a144c 100644 --- a/extensions/Bitwarden.Opaque/rust/Cargo.toml +++ b/extensions/Bitwarden.Opaque/rust/Cargo.toml @@ -9,18 +9,22 @@ crate-type = ["cdylib"] [dependencies] argon2 = "0.5.3" -digest = "0.10.7" -generic-array = "0.14.7" hex = "0.4.3" opaque-ke = { version = "3.0.0", features = ["std", "argon2"] } rand = "0.8.5" rand_chacha = "0.3.1" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" -voprf = "0.5.0" zeroizing-alloc = "0.1.0" -# Turn on LTO on release mode +# We want panic unwinding to be enabled, which would ensure the process it's not aborted +# when a panic occurs. This the default value, but we want to be explicit about it. + +[profile.dev] +panic = "unwind" + [profile.release] +panic = "unwind" +# Turn on LTO on release mode lto = true codegen-units = 1 From dcb0e2d78681cd96f210832a5aa36bcba6168f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 15:49:09 +0100 Subject: [PATCH 79/89] Update version --- extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj index 32a9c0cc..5f7eb65e 100644 --- a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj +++ b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj @@ -33,7 +33,7 @@ 0.1.0 beta - 2 + 3 $(PreReleaseVersionLabel).$(PreReleaseVersionIteration) From 8a421281ab6db8e4ff65148b26cde00bb3fe2cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 16:57:50 +0100 Subject: [PATCH 80/89] Add FFI panic, tested locally with miri --- .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 80 +++++++++++++++++++ .../Bitwarden.Opaque/rust/src/ffi/types.rs | 8 ++ 2 files changed, 88 insertions(+) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index bae6ab91..2e722f43 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -240,3 +240,83 @@ pub unsafe extern "C" fn finish_server_login( Response::ok1(response.session_key) }) } + +#[cfg(test)] +mod tests { + use std::ffi::{CString, c_char}; + + use crate::opaque::*; + + use super::{types::Buffer, *}; + + // Test for possible panics and/or undefined behavior. + // Ideally run using: + // + // cargo +nightly miri test + // RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target aarch64-apple-darwin --release + // RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test --target aarch64-apple-darwin --release + #[test] + fn test_ffi_no_panic() { + let user = "username"; + let user = CString::new(user).unwrap(); + let pass = "password"; + let pass = CString::new(pass).unwrap(); + let cfg = serde_json::to_string(&CipherConfiguration::default()).unwrap(); + let cfg = CString::new(cfg.as_str()).unwrap(); + + let _buf = Buffer::from_vec(vec![0; 32]); + let buf = || _buf.duplicate(); + + let _null_buf = Buffer::null(); + let null_buf = || _null_buf.duplicate(); + + let null_str_ptr: *const c_char = std::ptr::null(); + + unsafe { + start_client_registration(cfg.as_ptr(), pass.as_ptr()); + start_client_registration(null_str_ptr, pass.as_ptr()); + start_client_registration(null_str_ptr, null_str_ptr); + + start_server_registration(cfg.as_ptr(), buf(), buf(), user.as_ptr()); + start_server_registration(null_str_ptr, buf(), buf(), user.as_ptr()); + start_server_registration(cfg.as_ptr(), buf(), buf(), user.as_ptr()); + start_server_registration(cfg.as_ptr(), null_buf(), buf(), user.as_ptr()); + start_server_registration(cfg.as_ptr(), buf(), null_buf(), null_str_ptr); + + finish_client_registration(cfg.as_ptr(), buf(), buf(), pass.as_ptr()); + finish_client_registration(null_str_ptr, buf(), buf(), pass.as_ptr()); + finish_client_registration(cfg.as_ptr(), null_buf(), buf(), pass.as_ptr()); + finish_client_registration(cfg.as_ptr(), buf(), null_buf(), pass.as_ptr()); + finish_client_registration(cfg.as_ptr(), buf(), buf(), null_str_ptr); + + finish_server_registration(cfg.as_ptr(), buf()); + finish_server_registration(null_str_ptr, buf()); + finish_server_registration(cfg.as_ptr(), null_buf()); + + start_client_login(cfg.as_ptr(), pass.as_ptr()); + start_client_login(null_str_ptr, pass.as_ptr()); + start_client_login(cfg.as_ptr(), null_str_ptr); + + start_server_login(cfg.as_ptr(), buf(), buf(), buf(), user.as_ptr()); + start_server_login(null_str_ptr, buf(), buf(), buf(), user.as_ptr()); + start_server_login(cfg.as_ptr(), null_buf(), buf(), buf(), user.as_ptr()); + start_server_login(cfg.as_ptr(), buf(), null_buf(), buf(), user.as_ptr()); + start_server_login(cfg.as_ptr(), buf(), buf(), null_buf(), user.as_ptr()); + start_server_login(cfg.as_ptr(), buf(), buf(), buf(), null_str_ptr); + + finish_client_login(cfg.as_ptr(), buf(), buf(), pass.as_ptr()); + finish_client_login(null_str_ptr, buf(), buf(), pass.as_ptr()); + finish_client_login(cfg.as_ptr(), null_buf(), buf(), pass.as_ptr()); + finish_client_login(cfg.as_ptr(), buf(), null_buf(), pass.as_ptr()); + finish_client_login(cfg.as_ptr(), buf(), buf(), null_str_ptr); + + finish_server_login(cfg.as_ptr(), buf(), buf()); + finish_server_login(null_str_ptr, buf(), buf()); + finish_server_login(cfg.as_ptr(), null_buf(), buf()); + finish_server_login(cfg.as_ptr(), buf(), null_buf()); + + buf().free(); + null_buf().free(); + } + } +} diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs index f699e8fe..351af429 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs @@ -81,6 +81,14 @@ impl Buffer { let _ = unsafe { Vec::from_raw_parts(self.data, self.len, self.len) }; } } + + #[cfg(test)] + pub fn duplicate(&self) -> Buffer { + Buffer { + data: self.data, + len: self.len, + } + } } #[macro_export] From 0596a2f727046905f9a92e8ef7158f45246db1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Wed, 19 Mar 2025 19:42:36 +0100 Subject: [PATCH 81/89] Restore build/test/scan.yml --- .github/workflows/build.yml | 23 ----------------------- .github/workflows/scan.yml | 5 ----- .github/workflows/test.yml | 5 ----- 3 files changed, 33 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08f94c31..583b91d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,14 +8,8 @@ on: pull_request: jobs: - build_rust: - name: Build Rust Cross Platform - uses: ./.github/workflows/build-rust-cross-platform.yml - build-artifacts: name: Build artifacts - needs: - - build_rust runs-on: ubuntu-22.04 steps: @@ -27,20 +21,3 @@ jobs: - name: Build solution run: dotnet build - - - name: Download cross compiled library files - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: libopaque_ke_binding_all_files - path: extensions/Bitwarden.Opaque/rust/dist/ - - - name: Pack solution - run: dotnet pack -c Release --output ./nuget-output - - - name: Upload NuGet package - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: Bitwarden.Opaque.nupkg - if-no-files-found: error - path: | - ./nuget-output/Bitwarden.Opaque*.nupkg diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 9277cff0..e0cd62a9 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -74,11 +74,6 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - name: Install rust - uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable - with: - toolchain: stable - - name: Install SonarCloud scanner run: dotnet tool install dotnet-sonarscanner -g diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f903e5e2..c926768c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,11 +24,6 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - name: Install rust - uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable - with: - toolchain: stable - - name: Test solution run: dotnet test --configuration Debug --logger "trx;LogFileName=test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" From 5cdbab34b8a741d599ba9f7a4d7572d25f3fc05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 20 Mar 2025 13:49:30 +0100 Subject: [PATCH 82/89] Remove unused json converter --- .../Bitwarden.Opaque/src/BitwardenLibrary.cs | 5 ++++ .../src/CipherConfiguration.cs | 27 ++----------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs index d8d34135..901b1996 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs @@ -43,6 +43,9 @@ public readonly List GetAllBuffers() } } + // These are all the functions defined in the rust library. + // Important: The function signatures must always match the signatures in the rust library. + [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] internal static partial void free_buffer(Buffer buf); @@ -74,6 +77,8 @@ public readonly List GetAllBuffers() [LibraryImport("opaque_ke_binding", StringMarshalling = StringMarshalling.Utf8)] internal static partial Response finish_server_login(string config, Buffer state, Buffer credential_finalization); + // This is an internal class to improve the FFI handling. It should not be created directly, + // and should be used only from inside the ExecuteFFIFunction callback. internal class FFIHandler { private FFIHandler() { } diff --git a/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs b/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs index 7411e05d..9feaef3f 100644 --- a/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs +++ b/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs @@ -1,30 +1,7 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Bitwarden.Opaque; -class KeyExchangeConverter : JsonConverter -{ - public override KeyExchange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString() ?? throw new JsonException("Invalid value"); - return value.ToLower() switch - { - "triple-dh" => KeyExchange.TripleDH, - "tripledh" => KeyExchange.TripleDH, // Handles both formats - _ => throw new JsonException($"Unknown value: {value}") - }; - } - - public override void Write(Utf8JsonWriter writer, KeyExchange value, JsonSerializerOptions options) - { - writer.WriteStringValue(value switch - { - KeyExchange.TripleDH => "triple-dh", - }); - } -} - /// A VOPRF ciphersuite [JsonConverter(typeof(JsonStringEnumConverter))] public enum OprfCs @@ -42,7 +19,7 @@ public enum KeGroup } /// The key exchange protocol to use in the login step -[JsonConverter(typeof(KeyExchangeConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum KeyExchange { /// The Triple Diffie-Hellman key exchange implementation From eab76852e00be631b5bcb6100b316114ff5e1144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 20 Mar 2025 14:08:07 +0100 Subject: [PATCH 83/89] Reduce visibilities --- .../Bitwarden.Opaque/src/BitwardenLibrary.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs index 901b1996..26c00b32 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs @@ -16,8 +16,8 @@ internal static partial class BitwardenLibrary [StructLayout(LayoutKind.Sequential)] internal struct Buffer { - public IntPtr data; - public nint size; + internal IntPtr data; + internal nint size; } /// @@ -28,16 +28,16 @@ internal struct Buffer [StructLayout(LayoutKind.Sequential)] internal struct Response { - public nint error; - public Buffer error_message; + internal nint error; + internal Buffer error_message; - public Buffer data1; - public Buffer data2; - public Buffer data3; - public Buffer data4; + internal Buffer data1; + internal Buffer data2; + internal Buffer data3; + internal Buffer data4; // Utility function to get all buffers as a list - public readonly List GetAllBuffers() + internal readonly List GetAllBuffers() { return [data1, data2, data3, data4]; } From 79b091a908d1a528fac0725f1af23f334fb96cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 20 Mar 2025 14:08:27 +0100 Subject: [PATCH 84/89] Simplify response creation --- .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index 2e722f43..179bf2a8 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -41,8 +41,7 @@ pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response { let (server_setup, server_registration) = try_ffi!(crate::opaque::register_seeded_fake_config(seed)); - - Response::ok2(server_setup, server_registration) + Response::ok([server_setup, server_registration]) }) } @@ -61,8 +60,7 @@ pub unsafe extern "C" fn start_client_registration( let mut config = try_ffi!(CipherConfiguration::from_str(config)); let result = try_ffi!(config.start_client_registration(password)); - - Response::ok2(result.registration_request, result.state) + Response::ok([result.registration_request, result.state]) }) } @@ -89,8 +87,7 @@ pub unsafe extern "C" fn start_server_registration( registration_request, username )); - - Response::ok2(response.registration_response, response.server_setup) + Response::ok([response.registration_response, response.server_setup]) }) } @@ -114,12 +111,11 @@ pub unsafe extern "C" fn finish_client_registration( let response = try_ffi!(config.finish_client_registration(state, registration_response, password)); - - Response::ok3( + Response::ok([ response.registration_upload, response.export_key, response.server_s_pk, - ) + ]) }) } @@ -138,7 +134,7 @@ pub unsafe extern "C" fn finish_server_registration( let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_server_registration(registration_upload)); - Response::ok1(response.server_registration) + Response::ok([response.server_registration]) }) } @@ -157,7 +153,7 @@ pub unsafe extern "C" fn start_client_login( let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.start_client_login(password)); - Response::ok2(response.credential_request, response.state) + Response::ok([response.credential_request, response.state]) }) } @@ -187,7 +183,7 @@ pub unsafe extern "C" fn start_server_login( credential_request, username, )); - Response::ok2(response.credential_response, response.state) + Response::ok([response.credential_response, response.state]) }) } @@ -210,12 +206,12 @@ pub unsafe extern "C" fn finish_client_login( let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_client_login(state, credential_response, password)); - Response::ok4( + Response::ok([ response.credential_finalization, response.session_key, response.export_key, response.server_s_pk, - ) + ]) }) } @@ -236,8 +232,7 @@ pub unsafe extern "C" fn finish_server_login( let mut config = try_ffi!(CipherConfiguration::from_str(config)); let response = try_ffi!(config.finish_server_login(state, credential_finalization)); - - Response::ok1(response.session_key) + Response::ok([response.session_key]) }) } From 03202fd8d1611b81cd3ac1a6ae098f120f31e1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 20 Mar 2025 14:31:02 +0100 Subject: [PATCH 85/89] Improve rust response handling --- .../Bitwarden.Opaque/rust/src/ffi/types.rs | 78 ++++++------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs index 351af429..35050231 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs @@ -109,60 +109,36 @@ pub struct Response { pub error: usize, pub error_message: Buffer, - // TODO: This is a way of returning multiple values without having different return FFI types. - // Ideally we'd have a separate type per return type? Or maybe a better way to represent this. - pub data1: Buffer, - pub data2: Buffer, - pub data3: Buffer, - pub data4: Buffer, + // This is a way of returning multiple values without having different return FFI types. + // Currently the API only returns byte arrays so this is a good fit for now. + // Important: The length of this array must match exactly with the length of + // the C# BitwardenLibrary.Response struct. It also must be greater than or + // equal than the implementations of the AllowedSize trait below. + pub data: [Buffer; 4], } -impl Response { - pub fn ok1(data1: Vec) -> Self { - Response { - error: 0, - error_message: Buffer::null(), - - data1: Buffer::from_vec(data1), - data2: Buffer::null(), - data3: Buffer::null(), - data4: Buffer::null(), - } - } - - pub fn ok2(data1: Vec, data2: Vec) -> Self { - Response { - error: 0, - error_message: Buffer::null(), - - data1: Buffer::from_vec(data1), - data2: Buffer::from_vec(data2), - data3: Buffer::null(), - data4: Buffer::null(), - } - } - - pub fn ok3(data1: Vec, data2: Vec, data3: Vec) -> Self { - Response { - error: 0, - error_message: Buffer::null(), +pub(crate) trait AllowedSize {} +impl AllowedSize for [T; 1] {} +impl AllowedSize for [T; 2] {} +impl AllowedSize for [T; 3] {} +impl AllowedSize for [T; 4] {} - data1: Buffer::from_vec(data1), - data2: Buffer::from_vec(data2), - data3: Buffer::from_vec(data3), - data4: Buffer::null(), - } - } - - pub fn ok4(data1: Vec, data2: Vec, data3: Vec, data4: Vec) -> Self { +impl Response { + pub(crate) fn ok(data: [Vec; N]) -> Self + where + [Vec; N]: AllowedSize, + { + let mut iter = data.into_iter().fuse(); + let data = std::array::from_fn(|_| match iter.next() { + Some(vec) => Buffer::from_vec(vec), + None => Buffer::null(), + }); + + debug_assert!(iter.next().is_none()); Response { error: 0, error_message: Buffer::null(), - - data1: Buffer::from_vec(data1), - data2: Buffer::from_vec(data2), - data3: Buffer::from_vec(data3), - data4: Buffer::from_vec(data4), + data, } } @@ -178,11 +154,7 @@ impl Response { Response { error, error_message: Buffer::from_vec(message.into_bytes()), - - data1: Buffer::null(), - data2: Buffer::null(), - data3: Buffer::null(), - data4: Buffer::null(), + data: std::array::from_fn(|_| Buffer::null()), } } } From 6d6ade8de9d01c8ee54a594b577ce36557d88da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Thu, 20 Mar 2025 17:37:13 +0100 Subject: [PATCH 86/89] Fix lints, simplify macro, add toolchain --- extensions/Bitwarden.Opaque/rust/Cargo.lock | 8 +- .../Bitwarden.Opaque/rust/rust-toolchain.toml | 3 + .../Bitwarden.Opaque/rust/src/ffi/mod.rs | 18 +-- .../Bitwarden.Opaque/rust/src/ffi/types.rs | 16 +-- .../Bitwarden.Opaque/rust/src/opaque/mod.rs | 107 ++++++++++-------- 5 files changed, 82 insertions(+), 70 deletions(-) create mode 100644 extensions/Bitwarden.Opaque/rust/rust-toolchain.toml diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.lock b/extensions/Bitwarden.Opaque/rust/Cargo.lock index 2a5f335a..8bc6d84a 100644 --- a/extensions/Bitwarden.Opaque/rust/Cargo.lock +++ b/extensions/Bitwarden.Opaque/rust/Cargo.lock @@ -22,9 +22,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64ct" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "blake2" @@ -293,9 +293,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "once_cell" -version = "1.21.0" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "opaque-ke" diff --git a/extensions/Bitwarden.Opaque/rust/rust-toolchain.toml b/extensions/Bitwarden.Opaque/rust/rust-toolchain.toml new file mode 100644 index 00000000..73cb934d --- /dev/null +++ b/extensions/Bitwarden.Opaque/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs index 179bf2a8..90b9d403 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs @@ -11,7 +11,7 @@ mod types; use types::*; /// # Safety -/// All the limitations of [std::ffi::CStr::from_ptr] apply, mainly: +/// All the limitations of [`std::ffi::CStr::from_ptr`] apply, mainly: /// - The pointer must be valid and point to a null-terminated byte string. /// - The memory must be valid for the duration of the call and not modified by other threads. unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a str, Error> { @@ -46,7 +46,7 @@ pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response { } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_client_registration( @@ -65,7 +65,7 @@ pub unsafe extern "C" fn start_client_registration( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_registration( @@ -92,7 +92,7 @@ pub unsafe extern "C" fn start_server_registration( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_registration( @@ -120,7 +120,7 @@ pub unsafe extern "C" fn finish_client_registration( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_server_registration( @@ -139,7 +139,7 @@ pub unsafe extern "C" fn finish_server_registration( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_client_login( @@ -158,7 +158,7 @@ pub unsafe extern "C" fn start_client_login( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn start_server_login( @@ -188,7 +188,7 @@ pub unsafe extern "C" fn start_server_login( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_client_login( @@ -216,7 +216,7 @@ pub unsafe extern "C" fn finish_client_login( } /// # Safety -/// This function must follow the same safety rules as [parse_str] and [Buffer::as_slice]. +/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`]. /// The caller must ensure that the [Response] is correctly freed after use. #[unsafe(no_mangle)] pub unsafe extern "C" fn finish_server_login( diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs index 35050231..38238338 100644 --- a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs +++ b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs @@ -22,7 +22,7 @@ pub struct Buffer { /// A struct to represent a buffer of data. /// Important: The structure of this type must match the structure -/// of the Buffer type in the C# BitwardenLibrary, both in field type and order. +/// of the Buffer type in the C# `BitwardenLibrary`, both in field type and order. impl Buffer { pub fn null() -> Self { Buffer { @@ -42,8 +42,8 @@ impl Buffer { } /// # Safety - /// All the limitations of [std::slice::from_raw_parts] apply, mainly: - /// - The pointer must be either null or valid for reads up to [Buffer::len] bytes. + /// All the limitations of [`std::slice::from_raw_parts`] apply, mainly: + /// - The pointer must be either null or valid for reads up to [`Buffer::len`] bytes. /// - The memory must be valid for the duration of the call and not modified by other threads. pub unsafe fn as_slice_optional(&self) -> Result, Error> { if self.data.is_null() { @@ -64,8 +64,8 @@ impl Buffer { } /// # Safety - /// All the limitations of [std::slice::from_raw_parts] apply, mainly: - /// - The pointer must be either null or valid for reads up to [Buffer::len] bytes. + /// All the limitations of [`std::slice::from_raw_parts`] apply, mainly: + /// - The pointer must be either null or valid for reads up to [`Buffer::len`] bytes. /// - The memory must be valid for the duration of the call and not modified by other threads. pub unsafe fn as_slice(&self) -> Result<&[u8], Error> { unsafe { self.as_slice_optional() }? @@ -103,7 +103,7 @@ macro_rules! try_ffi { /// A struct to represent a response from the rust library. /// Important: The structure of this type must match the structure of the -/// Response type in the C# BitwardenLibrary, both in field type and order. +/// Response type in the C# `BitwardenLibrary`, both in field type and order. #[repr(C)] pub struct Response { pub error: usize, @@ -112,7 +112,7 @@ pub struct Response { // This is a way of returning multiple values without having different return FFI types. // Currently the API only returns byte arrays so this is a good fit for now. // Important: The length of this array must match exactly with the length of - // the C# BitwardenLibrary.Response struct. It also must be greater than or + // the C# `BitwardenLibrary.Response` struct. It also must be greater than or // equal than the implementations of the AllowedSize trait below. pub data: [Buffer; 4], } @@ -147,7 +147,7 @@ impl Response { let (error, message) = match error { Error::InvalidInput(name) => (1, name), Error::InvalidConfig(error) => (2, error), - Error::Protocol(e) => (3, format!("{:?}", e)), + Error::Protocol(e) => (3, format!("{e:?}")), Error::InternalError(error) => (4, error), }; diff --git a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs index 661b7c72..30dabead 100644 --- a/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs +++ b/extensions/Bitwarden.Opaque/rust/src/opaque/mod.rs @@ -84,6 +84,20 @@ impl<'a> OpaqueUtil<'a> for RistrettoTripleDhIdentitySuite<'a> { } } +// This generic utility function is used to dynamically dispatch to the correct cipher suite +fn with_variants( + config: &mut CipherConfiguration, + func: impl FnOnce(&mut dyn OpaqueImpl) -> Result, +) -> Result { + if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(config) { + return func(&mut suite); + }; + if let Some(mut suite) = RistrettoTripleDhIdentitySuite::as_variant(config) { + return func(&mut suite); + }; + Err(invalid_config(config)) +} + pub fn register_seeded_fake_config(seed: [u8; 32]) -> Result<(Vec, Vec), Error> { use rand::RngCore as _; @@ -108,20 +122,6 @@ pub fn register_seeded_fake_config(seed: [u8; 32]) -> Result<(Vec, Vec), Ok((server_start.server_setup, server_finish.server_registration)) } -// This generic utility function is used to dynamically dispatch to the correct cipher suite -fn with_variants( - config: &mut CipherConfiguration, - func: impl FnOnce(&mut dyn OpaqueImpl) -> Result, -) -> Result { - if let Some(mut suite) = RistrettoTripleDhArgonSuite::as_variant(config) { - return func(&mut suite); - }; - if let Some(mut suite) = RistrettoTripleDhIdentitySuite::as_variant(config) { - return func(&mut suite); - }; - Err(invalid_config(config)) -} - // The opaque-ke crate uses a lot of generic traits, which are difficult to handle in FFI. // This trait implements dynamic dispatch to allow using opaque-ke without generics. pub trait OpaqueImpl { @@ -250,21 +250,17 @@ impl OpaqueImpl for CipherConfiguration { } } -macro_rules! implement_cipher_suites { - ( $( $name:ident ),+ $(,)? ) => { - // Check that any type implements the required trait - const fn _assert_impl_traits<'a, T: OpaqueUtil<'a>>() {} - const _: () = { $( _assert_impl_traits::<$name>(); )+ }; - - // Implement OpaqueImpl for each cipher suite. - // This is just copying the implementation on RistrettoTripleDhArgonSuite and wrapping it in $()+ - $(impl OpaqueImpl for $name<'_> { +// Implement OpaqueImpl for each cipher suite. The code is entirely the same except for the impl OpaqueImpl for +macro_rules! implement_cipher_suite { + ( $type:ty ) => { + impl crate::opaque::OpaqueImpl for $type { fn start_client_registration( &mut self, password: &str, - ) -> Result { - let result = ClientRegistration::::start(self.get_rng(), password.as_bytes())?; - Ok(types::ClientRegistrationStartResult { + ) -> Result { + let result = + ClientRegistration::::start(self.get_rng(), password.as_bytes())?; + Ok(crate::opaque::types::ClientRegistrationStartResult { registration_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) @@ -274,13 +270,17 @@ macro_rules! implement_cipher_suites { server_setup: Option<&[u8]>, registration_request: &[u8], username: &str, - ) -> Result { + ) -> Result { let server_setup = match server_setup { Some(server_setup) => ServerSetup::::deserialize(server_setup)?, None => ServerSetup::::new(self.get_rng()), }; - let result = ServerRegistration::start(&server_setup, RegistrationRequest::deserialize(registration_request)?, username.as_bytes())?; - Ok(types::ServerRegistrationStartResult { + let result = ServerRegistration::start( + &server_setup, + RegistrationRequest::deserialize(registration_request)?, + username.as_bytes(), + )?; + Ok(crate::opaque::types::ServerRegistrationStartResult { registration_response: result.message.serialize().to_vec(), server_setup: server_setup.serialize().to_vec(), }) @@ -290,13 +290,14 @@ macro_rules! implement_cipher_suites { state: &[u8], registration_response: &[u8], password: &str, - ) -> Result { + ) -> Result { let state = ClientRegistration::::deserialize(state)?; let ksf = self.get_ksf()?; let response = RegistrationResponse::deserialize(registration_response)?; - let params = ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)); + let params = + ClientRegistrationFinishParameters::new(Identifiers::default(), Some(&ksf)); let result = state.finish(self.get_rng(), password.as_bytes(), response, params)?; - Ok(types::ClientRegistrationFinishResult { + Ok(crate::opaque::types::ClientRegistrationFinishResult { registration_upload: result.message.serialize().to_vec(), export_key: result.export_key.to_vec(), server_s_pk: result.server_s_pk.serialize().to_vec(), @@ -305,10 +306,10 @@ macro_rules! implement_cipher_suites { fn finish_server_registration( &mut self, registration_upload: &[u8], - ) -> Result { + ) -> Result { let upload = RegistrationUpload::::deserialize(registration_upload)?; let registration = ServerRegistration::finish(upload); - Ok(types::ServerRegistrationFinishResult { + Ok(crate::opaque::types::ServerRegistrationFinishResult { server_registration: registration.serialize().to_vec(), }) } @@ -316,9 +317,9 @@ macro_rules! implement_cipher_suites { fn start_client_login( &mut self, password: &str, - ) -> Result { + ) -> Result { let result = ClientLogin::::start(self.get_rng(), password.as_bytes())?; - Ok(types::ClientLoginStartResult { + Ok(crate::opaque::types::ClientLoginStartResult { credential_request: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) @@ -329,10 +330,10 @@ macro_rules! implement_cipher_suites { server_registration: &[u8], credential_request: &[u8], username: &str, - ) -> Result { - + ) -> Result { let server_setup = ServerSetup::::deserialize(server_setup)?; - let server_registration = ServerRegistration::::deserialize(server_registration)?; + let server_registration = + ServerRegistration::::deserialize(server_registration)?; let credential_request = CredentialRequest::deserialize(credential_request)?; let result = ServerLogin::start( @@ -343,7 +344,7 @@ macro_rules! implement_cipher_suites { username.as_bytes(), ServerLoginStartParameters::default(), )?; - Ok(types::ServerLoginStartResult { + Ok(crate::opaque::types::ServerLoginStartResult { credential_response: result.message.serialize().to_vec(), state: result.state.serialize().to_vec(), }) @@ -353,12 +354,17 @@ macro_rules! implement_cipher_suites { state: &[u8], credential_response: &[u8], password: &str, - ) -> Result { + ) -> Result { let client_login = ClientLogin::::deserialize(state)?; let ksf = self.get_ksf()?; - let params = ClientLoginFinishParameters::new(None, Identifiers::default(), Some(&ksf)); - let result = client_login.finish(password.as_bytes(), CredentialResponse::deserialize(credential_response)?, params)?; - Ok(types::ClientLoginFinishResult { + let params = + ClientLoginFinishParameters::new(None, Identifiers::default(), Some(&ksf)); + let result = client_login.finish( + password.as_bytes(), + CredentialResponse::deserialize(credential_response)?, + params, + )?; + Ok(crate::opaque::types::ClientLoginFinishResult { credential_finalization: result.message.serialize().to_vec(), session_key: result.session_key.to_vec(), export_key: result.export_key.to_vec(), @@ -369,15 +375,18 @@ macro_rules! implement_cipher_suites { &mut self, state: &[u8], credential_finalization: &[u8], - ) -> Result { + ) -> Result { let server_login = ServerLogin::::deserialize(state)?; - let result = server_login.finish(CredentialFinalization::deserialize(credential_finalization)?)?; - Ok(types::ServerLoginFinishResult { + let result = server_login.finish(CredentialFinalization::deserialize( + credential_finalization, + )?)?; + Ok(crate::opaque::types::ServerLoginFinishResult { session_key: result.session_key.to_vec(), }) } - })+ + } }; } -implement_cipher_suites!(RistrettoTripleDhArgonSuite, RistrettoTripleDhIdentitySuite); +implement_cipher_suite!(RistrettoTripleDhArgonSuite<'_>); +implement_cipher_suite!(RistrettoTripleDhIdentitySuite<'_>); From 49dee36801a6311605da8b51e57daadf1e072933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Fri, 21 Mar 2025 15:50:19 +0100 Subject: [PATCH 87/89] Expand tests, always build native in release mode, also allow csproj build to be used even in release mode --- .../src/Bitwarden.Opaque.csproj | 39 ++++--- extensions/Bitwarden.Opaque/tests/Tests.cs | 108 ++++++++++++------ 2 files changed, 95 insertions(+), 52 deletions(-) diff --git a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj index 5f7eb65e..d7d2207d 100644 --- a/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj +++ b/extensions/Bitwarden.Opaque/src/Bitwarden.Opaque.csproj @@ -43,10 +43,13 @@ - - - ../rust/target/debug/ + + + true + false + $(BuildOpaqueLib) + $(NETCoreSdkRuntimeIdentifier) @@ -59,27 +62,27 @@ libopaque_ke_binding.dylib - - + + + + + + + + + + - + + - + + $(RustLibraryFile) Always true - runtimes/$(CurrentRID)/native + runtimes/ - - - - - - Always - true - runtimes/ - - \ No newline at end of file diff --git a/extensions/Bitwarden.Opaque/tests/Tests.cs b/extensions/Bitwarden.Opaque/tests/Tests.cs index c519cad1..aa2941dc 100644 --- a/extensions/Bitwarden.Opaque/tests/Tests.cs +++ b/extensions/Bitwarden.Opaque/tests/Tests.cs @@ -2,8 +2,27 @@ namespace Bitwarden.Opaque.Tests; using Xunit; -public class SampleTests +public class OpaqueTests { + // Lower the config values from default so the tests run fast + public readonly CipherConfiguration config = new() + { + OpaqueVersion = 3, + OprfCs = OprfCs.Ristretto255, + KeGroup = KeGroup.Ristretto255, + KeyExchange = KeyExchange.TripleDH, + Ksf = new Ksf + { + Algorithm = KsfAlgorithm.Argon2id, + Parameters = new KsfParameters + { + Iterations = 1, + Memory = 1024, + Parallelism = 1 + } + } + }; + [Fact] public void TestSeededRegistration() { @@ -23,10 +42,10 @@ public void TestSeededRegistration() Assert.Equal(serverRegistration1, serverRegistration2); } + [Fact] public void TestRegistration() { - // Get environment variables var username = "demo_username"; var password = "demo_password"; @@ -34,27 +53,6 @@ public void TestRegistration() var server = new BitwardenOpaqueServer(); var client = new BitwardenOpaqueClient(); - // Lower the config values so the test runs fast - var config = new CipherConfiguration - { - OpaqueVersion = 3, - OprfCs = OprfCs.Ristretto255, - KeGroup = KeGroup.Ristretto255, - KeyExchange = KeyExchange.TripleDH, - Ksf = new Ksf - { - Algorithm = KsfAlgorithm.Argon2id, - Parameters = new KsfParameters - { - Iterations = 1, - Memory = 1024, - Parallelism = 1 - } - } - }; - - ///// Registration - // Start the client registration var clientRegisterStartResult = client.StartRegistration(config, password); @@ -67,25 +65,67 @@ public void TestRegistration() // Client sends client_finish_result to server var serverRegisterFinishResult = server.FinishRegistration(config, clientRegisterFinishResult.registrationUpload); + // These two need to be stored in the server for future logins + Assert.NotNull(serverRegisterStartResult.serverSetup); Assert.NotNull(serverRegisterFinishResult.serverRegistration); - ///// Login + Assert.NotNull(clientRegisterFinishResult.exportKey); + Assert.NotNull(clientRegisterFinishResult.serverSPKey); + } + [Fact] + public void TestLogin() + { + var username = "demo_username"; + var password = "demo_password"; + + // These values have been obtained from a previous registration with the same user/pass + var serverSetup = Convert.FromBase64String("i1mHwGvcVd5iYedbbgYFnFNLOSbotw+Ltgvr+xkNaGp1exkmDOjmFlr5McxjGAff2zermIpPezwCzq1C95Tot+gKuJqwWJOJ6jMXIrg7dSx6+H1IvZnR7LFtI7ylYoMFTvOWPyMyfoPTHK/+IlzgB10bKYcuPb+W4vH224qrXAk="); + var serverRegistration = Convert.FromBase64String("ECHaam+JiZMa+lO8Rn6f5G4polvgvi468qUy1i6IaSu2L0Rh7XiQ5hm3KSu9doCGKIgfgeju/A5i8aefKZvxPtduytVRtaJm57+5jX7YYW1lv53jDIrvdgDwBt/xBO8Sghm8yzo/BUDDcYvClRx1N7rqk9CfaSQxkKQwKvgFeDtiXVWDj0i7MvVs6bBAFq9fprI8ahfdfeiQWx1Qcx5itCx7hlnzzvL4XwnNc3otFtz60PnYVsUpO+Mbe86ZGrNX"); + var serverSPKey = Convert.FromBase64String("8C9MWibFiO5PSCDXGQc2/jxTLCGBv6PC0jje9BOUhmk="); + + byte[]? previousSessionKey = null; + byte[]? previousExportKey = null; + + for (var i = 0; i < 2; i++) + { + // Create the OPAQUE Clients + var server = new BitwardenOpaqueServer(); + var client = new BitwardenOpaqueClient(); + + // Start the client login + var clientLoginStartResult = client.StartLogin(config, password); - // Start the client login - var clientLoginStartResult = client.StartLogin(config, password); + // Client sends login_start to server + var serverLoginStartResult = server.StartLogin(config, serverSetup, serverRegistration, clientLoginStartResult.credentialRequest, username); - // Client sends login_start to server - var serverLoginStartResult = server.StartLogin(config, serverRegisterStartResult.serverSetup, serverRegisterFinishResult.serverRegistration, clientLoginStartResult.credentialRequest, username); + // Server sends login_start_result to client + var clientLoginFinishResult = client.FinishLogin(config, clientLoginStartResult.state, serverLoginStartResult.credentialResponse, password); - // Server sends login_start_result to client - var clientLoginFinishResult = client.FinishLogin(config, clientLoginStartResult.state, serverLoginStartResult.credentialResponse, password); + // Client sends login_finish_result to server + var serverLoginFinishResult = server.FinishLogin(config, serverLoginStartResult.state, clientLoginFinishResult.credentialFinalization); - // Client sends login_finish_result to server - var serverLoginFinishResult = server.FinishLogin(config, serverLoginStartResult.state, clientLoginFinishResult.credentialFinalization); + // Session key must be the same in both client and server + Assert.NotNull(serverLoginFinishResult.sessionKey); + Assert.Equal(serverLoginFinishResult.sessionKey, clientLoginFinishResult.sessionKey); - Assert.NotNull(serverLoginFinishResult.sessionKey); + // SPKey must be the same as during registration + Assert.Equal(clientLoginFinishResult.serverSPKey, serverSPKey); - Assert.Equal(serverLoginFinishResult.sessionKey, clientLoginFinishResult.sessionKey); + if (i == 0) + { + previousSessionKey = serverLoginFinishResult.sessionKey; + previousExportKey = clientLoginFinishResult.exportKey; + } + else + { + // Session key must be different for each login + Assert.NotNull(serverLoginFinishResult.sessionKey); + Assert.NotEqual(previousSessionKey, serverLoginFinishResult.sessionKey); + // Export key must be the same for all logins + Assert.NotNull(clientLoginFinishResult.exportKey); + Assert.Equal(previousExportKey, clientLoginFinishResult.exportKey); + } + } } } From b0de1de5adf3a085adbdb7d5946983b0df266c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Fri, 21 Mar 2025 18:28:03 +0100 Subject: [PATCH 88/89] Add simple benchmarks --- bitwarden-dotnet.sln | 7 ++ .../perf/Bitwarden.Opaque.Benchmarks.csproj | 19 +++ .../Bitwarden.Opaque/perf/OpaqueBench.cs | 108 ++++++++++++++++++ extensions/Bitwarden.Opaque/perf/Program.cs | 4 + 4 files changed, 138 insertions(+) create mode 100644 extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj create mode 100644 extensions/Bitwarden.Opaque/perf/OpaqueBench.cs create mode 100644 extensions/Bitwarden.Opaque/perf/Program.cs diff --git a/bitwarden-dotnet.sln b/bitwarden-dotnet.sln index 412513eb..3cd6ee85 100644 --- a/bitwarden-dotnet.sln +++ b/bitwarden-dotnet.sln @@ -51,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque", "extensi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque.Tests", "extensions\Bitwarden.Opaque\tests\Bitwarden.Opaque.Tests.csproj", "{DC9DAB81-5ED7-4756-BF20-D594D87D2865}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque.Benchmarks", "extensions\Bitwarden.Opaque\perf\Bitwarden.Opaque.Benchmarks.csproj", "{96631E35-695B-4BAF-B1E7-4446437A5FC8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +118,10 @@ Global {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Release|Any CPU.Build.0 = Release|Any CPU + {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {5EC8B943-2E9E-437D-9FFC-D18B5DB4D7D0} = {695C76EF-1102-4805-970F-7C995EE54930} @@ -141,5 +147,6 @@ Global {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} = {695C76EF-1102-4805-970F-7C995EE54930} {B49E33DF-A672-4361-BECA-C3DA423BD7A9} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} {DC9DAB81-5ED7-4756-BF20-D594D87D2865} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} + {96631E35-695B-4BAF-B1E7-4446437A5FC8} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} EndGlobalSection EndGlobal diff --git a/extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj b/extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj new file mode 100644 index 00000000..4a6876aa --- /dev/null +++ b/extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + false + enable + enable + + + + + + + + + + + diff --git a/extensions/Bitwarden.Opaque/perf/OpaqueBench.cs b/extensions/Bitwarden.Opaque/perf/OpaqueBench.cs new file mode 100644 index 00000000..2162f83d --- /dev/null +++ b/extensions/Bitwarden.Opaque/perf/OpaqueBench.cs @@ -0,0 +1,108 @@ +using BenchmarkDotNet.Attributes; + +namespace Bitwarden.Opaque.Benchmarks; + +// dotnet run --project extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj -c Release -p:BuildOpaqueLib=true + +[MemoryDiagnoser] +public class OpaqueBench +{ + public BitwardenOpaqueServer server = new(); + public BitwardenOpaqueClient client = new(); + public CipherConfiguration config = CipherConfiguration.Default; + + public string username = "demo_username"; + public string password = "demo_password"; + + public byte[] serverSetup = null!; + public byte[] serverRegistration = null!; + + public byte[] clientRegistrationRequest = null!; + public byte[] clientRegistrationUpload = null!; + + public byte[] clientLoginCredentialRequest = null!; + public byte[] serverLoginState = null!; + public byte[] clientLoginCredentialFinalization = null!; + + [GlobalSetup] + public void Setup() + { + // Use the complete benchmarks to extract the data for the partial benchmarks + var registration = CompleteRegistration(); + serverSetup = registration.Item1; + serverRegistration = registration.Item2; + clientRegistrationRequest = registration.Item3; + clientRegistrationUpload = registration.Item4; + + var login = CompleteLogin(); + clientLoginCredentialRequest = login.Item1; + serverLoginState = login.Item2; + clientLoginCredentialFinalization = login.Item3; + } + + [Benchmark] + public (byte[], byte[]) SeededFakeRegistration() + { + var seed = new byte[32]; + return server.SeededFakeRegistration(seed); + } + + [Benchmark] + public (byte[], byte[], byte[], byte[]) CompleteRegistration() + { + var clientRegisterStartResult = client.StartRegistration(config, password); + var serverRegisterStartResult = server.StartRegistration(config, null, clientRegisterStartResult.registrationRequest, username); + var clientRegisterFinishResult = client.FinishRegistration(config, clientRegisterStartResult.state, serverRegisterStartResult.registrationResponse, password); + var serverRegisterFinishResult = server.FinishRegistration(config, clientRegisterFinishResult.registrationUpload); + return ( + serverRegisterStartResult.serverSetup, + serverRegisterFinishResult.serverRegistration, + clientRegisterStartResult.registrationRequest, + clientRegisterFinishResult.registrationUpload + ); + } + + [Benchmark] + public (byte[], byte[], byte[], byte[]) CompleteLogin() + { + var clientLoginStartResult = client.StartLogin(config, password); + var serverLoginStartResult = server.StartLogin(config, serverSetup, serverRegistration, clientLoginStartResult.credentialRequest, username); + var clientLoginFinishResult = client.FinishLogin(config, clientLoginStartResult.state, serverLoginStartResult.credentialResponse, password); + var serverLoginFinishResult = server.FinishLogin(config, serverLoginStartResult.state, clientLoginFinishResult.credentialFinalization); + return ( + clientLoginStartResult.credentialRequest, + serverLoginStartResult.state, + clientLoginFinishResult.credentialFinalization, + serverLoginFinishResult.sessionKey + ); + } + + [Benchmark] + public (byte[], byte[]) StartServerRegistration() + { + var result = server.StartRegistration(config, null, clientRegistrationRequest, username); + return (result.registrationResponse, result.serverSetup); + } + + [Benchmark] + public byte[] FinishServerRegistration() + { + var result = server.FinishRegistration(config, clientRegistrationUpload); + return result.serverRegistration; + } + + + [Benchmark] + public (byte[], byte[]) StartServerLogin() + { + var result = server.StartLogin(config, serverSetup, serverRegistration, clientLoginCredentialRequest, username); + return (result.credentialResponse, result.state); + } + + [Benchmark] + public byte[] FinishServerLogin() + { + var result = server.FinishLogin(config, serverLoginState, clientLoginCredentialFinalization); + return result.sessionKey; + } +} diff --git a/extensions/Bitwarden.Opaque/perf/Program.cs b/extensions/Bitwarden.Opaque/perf/Program.cs new file mode 100644 index 00000000..d25b2b7c --- /dev/null +++ b/extensions/Bitwarden.Opaque/perf/Program.cs @@ -0,0 +1,4 @@ +using System.Reflection; +using BenchmarkDotNet.Running; + +BenchmarkRunner.Run(Assembly.GetExecutingAssembly()); From 1990a32fc5a2ff57cd1b6048ee36facfd7cd4d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garci=CC=81a?= Date: Mon, 24 Mar 2025 17:36:25 +0100 Subject: [PATCH 89/89] Formatting --- extensions/Bitwarden.Opaque/perf/Program.cs | 2 +- extensions/Bitwarden.Opaque/src/BitwardenException.cs | 2 +- extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs | 2 +- extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs | 2 +- extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs | 3 +-- extensions/Bitwarden.Opaque/src/CipherConfiguration.cs | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/extensions/Bitwarden.Opaque/perf/Program.cs b/extensions/Bitwarden.Opaque/perf/Program.cs index d25b2b7c..ae6c326d 100644 --- a/extensions/Bitwarden.Opaque/perf/Program.cs +++ b/extensions/Bitwarden.Opaque/perf/Program.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using BenchmarkDotNet.Running; BenchmarkRunner.Run(Assembly.GetExecutingAssembly()); diff --git a/extensions/Bitwarden.Opaque/src/BitwardenException.cs b/extensions/Bitwarden.Opaque/src/BitwardenException.cs index bcd2f051..e44a45cb 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenException.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenException.cs @@ -1,4 +1,4 @@ -namespace Bitwarden.Opaque; +namespace Bitwarden.Opaque; /// /// A class to represent an exception thrown by the Bitwarden OPAQUE library. diff --git a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs index 26c00b32..fa4ef3a6 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenLibrary.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs index 1d97d2f4..682fb83a 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueClient.cs @@ -1,4 +1,4 @@ -namespace Bitwarden.Opaque; +namespace Bitwarden.Opaque; #pragma warning disable CA1822 // Mark members as static /// The result of diff --git a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs index 2c3221ac..73d27000 100644 --- a/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs +++ b/extensions/Bitwarden.Opaque/src/BitwardenOpaqueServer.cs @@ -1,4 +1,4 @@ -namespace Bitwarden.Opaque; +namespace Bitwarden.Opaque; #pragma warning disable CA1822 // Mark members as static @@ -13,7 +13,6 @@ public struct ServerRegistrationStartResult /// The result of public struct ServerRegistrationFinishResult - { /// The server registration, which needs to be persisted on the server for future logins. public byte[] serverRegistration; diff --git a/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs b/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs index 9feaef3f..588d3679 100644 --- a/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs +++ b/extensions/Bitwarden.Opaque/src/CipherConfiguration.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Bitwarden.Opaque;