From 418dbb374c175a698791cf83409bb2dfc6b0ba48 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 17:21:26 +0200 Subject: [PATCH 01/22] SDK integration in db seeder --- .gitignore | 2 + bitwarden-server.sln | 7 + ...nizationUsersControllerPerformanceTests.cs | 2 +- util/RustSdk/NativeMethods.cs | 58 + util/RustSdk/RustSdk.csproj | 26 + util/RustSdk/rust/Cargo.lock | 2933 +++++++++++++++++ util/RustSdk/rust/Cargo.toml | 19 + util/RustSdk/rust/build.rs | 9 + util/RustSdk/rust/src/lib.rs | 4 + util/Sdk2/dotnet/NativeMethods.cs | 56 + util/Seeder/Factories/UserSeeder.cs | 2 + util/Seeder/Seeder.csproj | 1 + 12 files changed, 3118 insertions(+), 1 deletion(-) create mode 100644 util/RustSdk/NativeMethods.cs create mode 100644 util/RustSdk/RustSdk.csproj create mode 100644 util/RustSdk/rust/Cargo.lock create mode 100644 util/RustSdk/rust/Cargo.toml create mode 100644 util/RustSdk/rust/build.rs create mode 100644 util/RustSdk/rust/src/lib.rs create mode 100644 util/Sdk2/dotnet/NativeMethods.cs diff --git a/.gitignore b/.gitignore index 65157bf4aa..578e095171 100644 --- a/.gitignore +++ b/.gitignore @@ -225,3 +225,5 @@ src/Notifications/Notifications.zip bitwarden_license/src/Portal/Portal.zip bitwarden_license/src/Sso/Sso.zip **/src/**/flags.json +NativeMethods.g.cs +util/RustSdk/rust/target diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 2ec8d86e0e..9c88501bb6 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -133,6 +133,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seede EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RustSdk", "util\RustSdk\RustSdk.csproj", "{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -337,6 +339,10 @@ Global {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU + {D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -391,6 +397,7 @@ Global {3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs index d77a41f52e..5c35ecbdb2 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -9,7 +9,7 @@ namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) { - [Theory(Skip = "Performance test")] + [Theory()] [InlineData(100)] [InlineData(60000)] public async Task GetAsync(int seats) diff --git a/util/RustSdk/NativeMethods.cs b/util/RustSdk/NativeMethods.cs new file mode 100644 index 0000000000..a118517929 --- /dev/null +++ b/util/RustSdk/NativeMethods.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Bit.RustSDK; + +public static partial class NativeMethods +{ + // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform + // Library path will search + // win => __DllName, __DllName.dll + // linux, osx => __DllName.so, __DllName.dylib + + static NativeMethods() + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver); + } + + static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != __DllName) return IntPtr.Zero; + + var path = "runtimes/"; + var extension = ""; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + path += "win-"; + extension = ".dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + path += "osx-"; + extension = ".dylib"; + } + else + { + path += "linux-"; + extension = ".so"; + } + + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + path += "x86"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + path += "x64"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + { + path += "arm64"; + } + + path += "/native/" + __DllName + extension; + + return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath); + } +} diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj new file mode 100644 index 0000000000..c4d52a6630 --- /dev/null +++ b/util/RustSdk/RustSdk.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + Bit.RustSDK + Bit.RustSDK + true + + + + + Always + true + runtimes/osx-arm64/native/libsdk.dylib + + + true + + + true + + + + \ No newline at end of file diff --git a/util/RustSdk/rust/Cargo.lock b/util/RustSdk/rust/Cargo.lock new file mode 100644 index 0000000000..af5b89a418 --- /dev/null +++ b/util/RustSdk/rust/Cargo.lock @@ -0,0 +1,2933 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[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", + "zeroize", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bitwarden-api-api" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "url", + "uuid", +] + +[[package]] +name = "bitwarden-api-identity" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "url", + "uuid", +] + +[[package]] +name = "bitwarden-core" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "base64", + "bitwarden-api-api", + "bitwarden-api-identity", + "bitwarden-crypto", + "bitwarden-error", + "bitwarden-state", + "bitwarden-uuid", + "chrono", + "getrandom 0.2.16", + "log", + "rand 0.8.5", + "reqwest", + "rustls", + "rustls-platform-verifier", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_qs", + "serde_repr", + "thiserror 1.0.69", + "uuid", + "zeroize", +] + +[[package]] +name = "bitwarden-crypto" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "aes", + "argon2", + "base64", + "bitwarden-error", + "cbc", + "chacha20poly1305", + "ciborium", + "coset", + "ed25519-dalek", + "generic-array", + "hkdf", + "hmac", + "num-bigint", + "num-traits", + "pbkdf2", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rayon", + "rsa", + "schemars 0.8.22", + "serde", + "serde_bytes", + "serde_repr", + "sha1", + "sha2", + "subtle", + "thiserror 1.0.69", + "typenum", + "uuid", + "zeroize", + "zeroizing-alloc", +] + +[[package]] +name = "bitwarden-error" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "bitwarden-error-macro", +] + +[[package]] +name = "bitwarden-error-macro" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitwarden-state" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "async-trait", + "thiserror 1.0.69", +] + +[[package]] +name = "bitwarden-uuid" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "bitwarden-uuid-macro", +] + +[[package]] +name = "bitwarden-uuid-macro" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +dependencies = [ + "quote", + "syn", +] + +[[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 = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coset" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8cc80f631f8307b887faca24dcc3abc427cd0367f6eb6188f6e8f5b7ad8fb" +dependencies = [ + "ciborium", + "ciborium-io", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "csbindgen" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26b9831049b947d154bba920e4124053def72447be6fb106a96f483874b482a" +dependencies = [ + "regex", + "syn", +] + +[[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", + "digest", + "fiat-crypto", + "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 = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "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 = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[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" +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 = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda84358ed17f1f354cf4b1909ad346e6c7bc2513e8c40eb08e0157aa13a9070" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "chrono", + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "sdk" +version = "0.1.0" +dependencies = [ + "bitwarden-core", + "bitwarden-crypto", + "csbindgen", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[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 = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "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 = "serde_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[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", + "rustversion", + "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-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[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 = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[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" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/util/RustSdk/rust/Cargo.toml b/util/RustSdk/rust/Cargo.toml new file mode 100644 index 0000000000..6fb51ff333 --- /dev/null +++ b/util/RustSdk/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sdk" +publish = false + +version = "0.1.0" +authors = ["Bitwarden Inc"] +edition = "2021" +homepage = "https://bitwarden.com" +repository = "https://github.com/bitwarden/server" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } +bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } + +[build-dependencies] +csbindgen = "=1.9.3" diff --git a/util/RustSdk/rust/build.rs b/util/RustSdk/rust/build.rs new file mode 100644 index 0000000000..0905afc22d --- /dev/null +++ b/util/RustSdk/rust/build.rs @@ -0,0 +1,9 @@ +fn main() { + csbindgen::Builder::default() + .input_extern_file("src/lib.rs") + .csharp_dll_name("libsdk") + .csharp_namespace("Bit.RustSDK") + .csharp_class_accessibility("public") + .generate_csharp_file("../NativeMethods.g.cs") + .unwrap(); +} diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs new file mode 100644 index 0000000000..d6b2fcdfa4 --- /dev/null +++ b/util/RustSdk/rust/src/lib.rs @@ -0,0 +1,4 @@ +#[no_mangle] +pub extern "C" fn my_add(x: i32, y: i32) -> i32 { + x + y +} diff --git a/util/Sdk2/dotnet/NativeMethods.cs b/util/Sdk2/dotnet/NativeMethods.cs new file mode 100644 index 0000000000..32efd38d4f --- /dev/null +++ b/util/Sdk2/dotnet/NativeMethods.cs @@ -0,0 +1,56 @@ +internal static unsafe partial class NativeMethods +{ + // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform + // Library path will search + // win => __DllName, __DllName.dll + // linux, osx => __DllName.so, __DllName.dylib + + static NativeMethods() + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver); + } + + static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName == __DllName) + { + var path = "runtimes/"; + var extension = ""; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + path += "win-"; + extension = ".dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + path += "osx-"; + extension = ".dylib"; + } + else + { + path += "linux-"; + extension = ".so"; + } + + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + path += "x86"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + path += "x64"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + { + path += "arm64"; + } + + path += "/native/" + __DllName + extension; + + return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath); + } + + return IntPtr.Zero; + } +} diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index 90cadf0b78..9c9d86e8d7 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -1,5 +1,6 @@ using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.Models; +using Bit.RustSDK; namespace Bit.Seeder.Factories; @@ -7,6 +8,7 @@ public class UserSeeder { public static User CreateUser(string email) { + Console.WriteLine(NativeMethods.my_add(2, 3)); return new User { Id = Guid.NewGuid(), diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj index 392f6434cc..4d7fbab767 100644 --- a/util/Seeder/Seeder.csproj +++ b/util/Seeder/Seeder.csproj @@ -20,6 +20,7 @@ + From 072f9f227811c3d25e3150d351bc1486ecb29243 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 28 Jul 2025 18:40:54 +0200 Subject: [PATCH 02/22] Add hash password --- util/RustSdk/RustSdkException.cs | 19 ++++++ util/RustSdk/RustSdkService.cs | 83 +++++++++++++++++++++++++++ util/RustSdk/RustSdkServiceFactory.cs | 30 ++++++++++ util/RustSdk/rust/src/lib.rs | 45 +++++++++++++++ util/Seeder/Factories/UserSeeder.cs | 6 +- 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 util/RustSdk/RustSdkException.cs create mode 100644 util/RustSdk/RustSdkService.cs create mode 100644 util/RustSdk/RustSdkServiceFactory.cs diff --git a/util/RustSdk/RustSdkException.cs b/util/RustSdk/RustSdkException.cs new file mode 100644 index 0000000000..52cbc3635f --- /dev/null +++ b/util/RustSdk/RustSdkException.cs @@ -0,0 +1,19 @@ +namespace Bit.RustSDK; + +/// +/// Exception thrown when the Rust SDK operations fail +/// +public class RustSdkException : Exception +{ + public RustSdkException() : base("An error occurred in the Rust SDK operation") + { + } + + public RustSdkException(string message) : base(message) + { + } + + public RustSdkException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/util/RustSdk/RustSdkService.cs b/util/RustSdk/RustSdkService.cs new file mode 100644 index 0000000000..dcb139dd0f --- /dev/null +++ b/util/RustSdk/RustSdkService.cs @@ -0,0 +1,83 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Bit.RustSDK; + +/// +/// Service implementation that provides a C# friendly interface to the Rust SDK +/// +public class RustSdkService +{ + /// + /// Adds two integers using the native implementation + /// + /// First integer + /// Second integer + /// The sum of x and y + public int Add(int x, int y) + { + try + { + return NativeMethods.my_add(x, y); + } + catch (Exception ex) + { + throw new RustSdkException($"Failed to perform addition operation: {ex.Message}", ex); + } + } + + /// + /// Hashes a password using the native implementation + /// + /// User email + /// User password + /// The hashed password as a string + /// Thrown when email or password is null + /// Thrown when email or password is empty + /// Thrown when the native operation fails + public unsafe string HashPassword(string email, string password) + { + // Convert strings to null-terminated byte arrays + var emailBytes = Encoding.UTF8.GetBytes(email + '\0'); + var passwordBytes = Encoding.UTF8.GetBytes(password + '\0'); + + try + { + fixed (byte* emailPtr = emailBytes) + fixed (byte* passwordPtr = passwordBytes) + { + var resultPtr = NativeMethods.hash_password(emailPtr, passwordPtr); + + var result = TakeAndDestroyRustString(resultPtr); + + return result; + } + } + catch (RustSdkException) + { + throw; // Re-throw our custom exceptions + } + catch (Exception ex) + { + throw new RustSdkException($"Failed to hash password: {ex.Message}", ex); + } + } + + private static unsafe string TakeAndDestroyRustString(byte* ptr) + { + if (ptr == null) + { + throw new RustSdkException("Pointer is null"); + } + + var result = Marshal.PtrToStringUTF8((IntPtr)ptr); + NativeMethods.free_c_string(ptr); + + if (result == null) + { + throw new RustSdkException("Failed to convert native result to string"); + } + + return result; + } +} diff --git a/util/RustSdk/RustSdkServiceFactory.cs b/util/RustSdk/RustSdkServiceFactory.cs new file mode 100644 index 0000000000..5a1e44eb22 --- /dev/null +++ b/util/RustSdk/RustSdkServiceFactory.cs @@ -0,0 +1,30 @@ +namespace Bit.RustSDK; + +/// +/// Factory for creating Rust SDK service instances +/// +public static class RustSdkServiceFactory +{ + /// + /// Creates a new instance of the Rust SDK service + /// + /// A new IRustSdkService instance + public static RustSdkService Create() + { + return new RustSdkService(); + } + + /// + /// Creates a singleton instance of the Rust SDK service (thread-safe) + /// + /// A singleton IRustSdkService instance + public static RustSdkService CreateSingleton() + { + return SingletonHolder.Instance; + } + + private static class SingletonHolder + { + internal static readonly RustSdkService Instance = new RustSdkService(); + } +} diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs index d6b2fcdfa4..1678944508 100644 --- a/util/RustSdk/rust/src/lib.rs +++ b/util/RustSdk/rust/src/lib.rs @@ -1,4 +1,49 @@ +use std::{ + ffi::{c_char, CStr, CString}, + num::NonZeroU32, +}; + +use bitwarden_crypto::{HashPurpose, Kdf, MasterKey}; + #[no_mangle] pub extern "C" fn my_add(x: i32, y: i32) -> i32 { x + y } + +/// # Safety +/// +/// The `email` and `password` pointers must be valid null-terminated C strings. +/// Both pointers must be non-null and point to valid memory for the duration of the function call. +#[no_mangle] +pub unsafe extern "C" fn hash_password( + email: *const c_char, + password: *const c_char, +) -> *const c_char { + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + + let email = CStr::from_ptr(email).to_str().unwrap(); + let password = CStr::from_ptr(password).to_str().unwrap(); + + let master_key = MasterKey::derive(password, email, &kdf).unwrap(); + + let res = master_key + .derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization) + .unwrap(); + + let res = CString::new(res).unwrap(); + + res.into_raw() +} + +/// # Safety +/// +/// The `str` pointer must be a valid pointer previously returned by `CString::into_raw` +/// and must not have already been freed. After calling this function, the pointer must not be used again. +#[no_mangle] +pub unsafe extern "C" fn free_c_string(str: *mut c_char) { + unsafe { + drop(CString::from_raw(str)); + } +} diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index 9c9d86e8d7..481f214637 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -8,12 +8,16 @@ public class UserSeeder { public static User CreateUser(string email) { + var nativeService = RustSdkServiceFactory.CreateSingleton(); Console.WriteLine(NativeMethods.my_add(2, 3)); + + var password = nativeService.HashPassword(email, "asdfasdfasdf"); + return new User { Id = Guid.NewGuid(), Email = email, - MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==", + MasterPassword = password, SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", From 75f11f68ac6518e602f279991acdc1fc615cec46 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 31 Jul 2025 10:20:53 +0200 Subject: [PATCH 03/22] Generate valid keys using rust --- util/DbSeederUtility/Program.cs | 10 +++- util/RustSdk/RustSdkService.cs | 47 +++++++++++++------ util/RustSdk/rust/Cargo.lock | 14 +++--- util/RustSdk/rust/Cargo.toml | 2 + util/RustSdk/rust/src/lib.rs | 34 ++++++++++++++ util/Seeder/Factories/UserSeeder.cs | 23 +++++---- .../Recipes/OrganizationWithUsersRecipe.cs | 18 +++---- 7 files changed, 108 insertions(+), 40 deletions(-) diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs index 2d75b31934..f6cadb65b1 100644 --- a/util/DbSeederUtility/Program.cs +++ b/util/DbSeederUtility/Program.cs @@ -1,7 +1,10 @@ -using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Models; +using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Recipes; using CommandDotNet; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Bit.DbSeederUtility; @@ -26,14 +29,17 @@ public class Program // Create service provider with necessary services var services = new ServiceCollection(); ServiceCollectionExtension.ConfigureServices(services); + services.TryAddScoped, PasswordHasher>(); + var serviceProvider = services.BuildServiceProvider(); // Get a scoped DB context using var scope = serviceProvider.CreateScope(); var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService(); + var passwordHasher = scopedServices.GetRequiredService>(); - var recipe = new OrganizationWithUsersRecipe(db); + var recipe = new OrganizationWithUsersRecipe(db, passwordHasher); recipe.Seed(name, users, domain); } } diff --git a/util/RustSdk/RustSdkService.cs b/util/RustSdk/RustSdkService.cs index dcb139dd0f..75ef72ab56 100644 --- a/util/RustSdk/RustSdkService.cs +++ b/util/RustSdk/RustSdkService.cs @@ -1,28 +1,40 @@ using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; namespace Bit.RustSDK; +public class UserKeys +{ + public required string MasterPasswordHash { get; set; } + public required string EncryptedUserKey { get; set; } + public required string PublicKey { get; set; } + public required string PrivateKey { get; set; } +} + /// /// Service implementation that provides a C# friendly interface to the Rust SDK /// public class RustSdkService { - /// - /// Adds two integers using the native implementation - /// - /// First integer - /// Second integer - /// The sum of x and y - public int Add(int x, int y) + private static readonly JsonSerializerOptions CaseInsensitiveOptions = new() { - try + PropertyNameCaseInsensitive = true + }; + + public unsafe UserKeys GenerateUserKeys(string email, string password) + { + var emailBytes = StringToRustString(email); + var passwordBytes = StringToRustString(password); + + fixed (byte* emailPtr = emailBytes) + fixed (byte* passwordPtr = passwordBytes) { - return NativeMethods.my_add(x, y); - } - catch (Exception ex) - { - throw new RustSdkException($"Failed to perform addition operation: {ex.Message}", ex); + var resultPtr = NativeMethods.generate_user_keys(emailPtr, passwordPtr); + + var result = TakeAndDestroyRustString(resultPtr); + + return JsonSerializer.Deserialize(result, CaseInsensitiveOptions)!; } } @@ -38,8 +50,8 @@ public class RustSdkService public unsafe string HashPassword(string email, string password) { // Convert strings to null-terminated byte arrays - var emailBytes = Encoding.UTF8.GetBytes(email + '\0'); - var passwordBytes = Encoding.UTF8.GetBytes(password + '\0'); + var emailBytes = StringToRustString(email); + var passwordBytes = StringToRustString(password); try { @@ -63,6 +75,11 @@ public class RustSdkService } } + private static byte[] StringToRustString(string str) + { + return Encoding.UTF8.GetBytes(str + '\0'); + } + private static unsafe string TakeAndDestroyRustString(byte* ptr) { if (ptr == null) diff --git a/util/RustSdk/rust/Cargo.lock b/util/RustSdk/rust/Cargo.lock index af5b89a418..85a819427b 100644 --- a/util/RustSdk/rust/Cargo.lock +++ b/util/RustSdk/rust/Cargo.lock @@ -184,7 +184,7 @@ dependencies = [ "serde_json", "serde_qs", "serde_repr", - "thiserror 1.0.69", + "thiserror 2.0.12", "uuid", "zeroize", ] @@ -220,7 +220,7 @@ dependencies = [ "sha1", "sha2", "subtle", - "thiserror 1.0.69", + "thiserror 2.0.12", "typenum", "uuid", "zeroize", @@ -252,7 +252,7 @@ version = "1.0.0" source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" dependencies = [ "async-trait", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -1802,7 +1802,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1897,6 +1897,8 @@ dependencies = [ "bitwarden-core", "bitwarden-crypto", "csbindgen", + "serde", + "serde_json", ] [[package]] @@ -1970,9 +1972,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", diff --git a/util/RustSdk/rust/Cargo.toml b/util/RustSdk/rust/Cargo.toml index 6fb51ff333..82e1c5777c 100644 --- a/util/RustSdk/rust/Cargo.toml +++ b/util/RustSdk/rust/Cargo.toml @@ -14,6 +14,8 @@ crate-type = ["cdylib"] [dependencies] bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } +serde = "=1.0.219" +serde_json = "=1.0.141" [build-dependencies] csbindgen = "=1.9.3" diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs index 1678944508..27dc3b2c4c 100644 --- a/util/RustSdk/rust/src/lib.rs +++ b/util/RustSdk/rust/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::missing_safety_doc)] use std::{ ffi::{c_char, CStr, CString}, num::NonZeroU32, @@ -10,6 +11,39 @@ pub extern "C" fn my_add(x: i32, y: i32) -> i32 { x + y } +#[no_mangle] +pub unsafe extern "C" fn generate_user_keys( + email: *const c_char, + password: *const c_char, +) -> *const c_char { + // TODO: We might want to make KDF configurable in the future. + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(600_000).unwrap(), + }; + + let email = CStr::from_ptr(email).to_str().unwrap(); + let password = CStr::from_ptr(password).to_str().unwrap(); + + let master_key = MasterKey::derive(password, email, &kdf).unwrap(); + let master_password_hash = master_key + .derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization) + .unwrap(); + let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap(); + let keys = user_key.make_key_pair().unwrap(); + + let json = serde_json::json!({ + "masterPasswordHash": master_password_hash, + "encryptedUserKey": encrypted_user_key.to_string(), + "publicKey": keys.public.to_string(), + "privateKey": keys.private.to_string(), + }) + .to_string(); + + let result = CString::new(json).unwrap(); + + result.into_raw() +} + /// # Safety /// /// The `email` and `password` pointers must be valid null-terminated C strings. diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index 481f214637..80d6c13159 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -1,31 +1,36 @@ using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Infrastructure.EntityFramework.Models; using Bit.RustSDK; +using Microsoft.AspNetCore.Identity; namespace Bit.Seeder.Factories; public class UserSeeder { - public static User CreateUser(string email) + + public static User CreateUser(IPasswordHasher passwordHasher, string email) { var nativeService = RustSdkServiceFactory.CreateSingleton(); - Console.WriteLine(NativeMethods.my_add(2, 3)); + var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf"); - var password = nativeService.HashPassword(email, "asdfasdfasdf"); - - return new User + var user = new User { Id = Guid.NewGuid(), Email = email, - MasterPassword = password, + MasterPassword = null, SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", - Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", - PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", - PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=", + Key = keys.EncryptedUserKey, + PublicKey = keys.PublicKey, + PrivateKey = keys.PrivateKey, ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 600_000, }; + + user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash); + + return user; } } diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs index fb06c091ae..776a32e5bb 100644 --- a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -1,36 +1,38 @@ -using Bit.Infrastructure.EntityFramework.Models; +using Bit.Core.Services; +using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Factories; using LinqToDB.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; namespace Bit.Seeder.Recipes; -public class OrganizationWithUsersRecipe(DatabaseContext db) +public class OrganizationWithUsersRecipe(DatabaseContext db, IPasswordHasher passwordHasher) { public Guid Seed(string name, int users, string domain) { var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); - var user = UserSeeder.CreateUser($"admin@{domain}"); + var user = UserSeeder.CreateUser(passwordHasher, $"admin@{domain}"); var orgUser = organization.CreateOrganizationUser(user); var additionalUsers = new List(); var additionalOrgUsers = new List(); for (var i = 0; i < users; i++) { - var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); + var additionalUser = UserSeeder.CreateUser(passwordHasher, $"user{i}@{domain}"); additionalUsers.Add(additionalUser); additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); } - db.Add(organization); + //db.Add(organization); db.Add(user); - db.Add(orgUser); + //db.Add(orgUser); db.SaveChanges(); // Use LinqToDB's BulkCopy for significant better performance - db.BulkCopy(additionalUsers); - db.BulkCopy(additionalOrgUsers); + //db.BulkCopy(additionalUsers); + //db.BulkCopy(additionalOrgUsers); return organization.Id; } From 3132e09e2195cb9dc209d2f20e40a7792c530afa Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 31 Jul 2025 14:48:48 +0200 Subject: [PATCH 04/22] Wire up crypto logic for sharing org key --- ...nizationUsersControllerPerformanceTests.cs | 5 +- util/RustSdk/RustSdkService.cs | 62 ++++++++-------- util/RustSdk/RustSdkServiceFactory.cs | 11 +-- util/RustSdk/rust/Cargo.lock | 27 +++---- util/RustSdk/rust/Cargo.toml | 5 +- util/RustSdk/rust/src/lib.rs | 70 +++++++++++++------ util/Seeder/Factories/OrganizationSeeder.cs | 26 ++++--- util/Seeder/Factories/UserSeeder.cs | 4 +- .../Recipes/OrganizationWithUsersRecipe.cs | 18 ++--- 9 files changed, 133 insertions(+), 95 deletions(-) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs index 5c35ecbdb2..4c701344a4 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -1,7 +1,9 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; +using Bit.Infrastructure.EntityFramework.Models; using Bit.Seeder.Recipes; +using Microsoft.AspNetCore.Identity; using Xunit; using Xunit.Abstractions; @@ -18,7 +20,8 @@ public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOu var client = factory.CreateClient(); var db = factory.GetDatabaseContext(); - var seeder = new OrganizationWithUsersRecipe(db); + var passwordHasher = factory.Services.CreateScope().ServiceProvider.GetService>(); + var seeder = new OrganizationWithUsersRecipe(db, passwordHasher); var orgId = seeder.Seed("Org", seats, "large.test"); diff --git a/util/RustSdk/RustSdkService.cs b/util/RustSdk/RustSdkService.cs index 75ef72ab56..ee01d56fee 100644 --- a/util/RustSdk/RustSdkService.cs +++ b/util/RustSdk/RustSdkService.cs @@ -7,11 +7,26 @@ namespace Bit.RustSDK; public class UserKeys { public required string MasterPasswordHash { get; set; } + /// + /// Base64 encoded UserKey + /// + public required string Key { get; set; } public required string EncryptedUserKey { get; set; } public required string PublicKey { get; set; } public required string PrivateKey { get; set; } } +public class OrganizationKeys +{ + /// + /// Base64 encoded SymmetricCryptoKey + /// + public required string Key { get; set; } + + public required string PublicKey { get; set; } + public required string PrivateKey { get; set; } +} + /// /// Service implementation that provides a C# friendly interface to the Rust SDK /// @@ -38,43 +53,32 @@ public class RustSdkService } } - /// - /// Hashes a password using the native implementation - /// - /// User email - /// User password - /// The hashed password as a string - /// Thrown when email or password is null - /// Thrown when email or password is empty - /// Thrown when the native operation fails - public unsafe string HashPassword(string email, string password) + public unsafe OrganizationKeys GenerateOrganizationKeys() { - // Convert strings to null-terminated byte arrays - var emailBytes = StringToRustString(email); - var passwordBytes = StringToRustString(password); + var resultPtr = NativeMethods.generate_organization_keys(); - try - { - fixed (byte* emailPtr = emailBytes) - fixed (byte* passwordPtr = passwordBytes) - { - var resultPtr = NativeMethods.hash_password(emailPtr, passwordPtr); + var result = TakeAndDestroyRustString(resultPtr); - var result = TakeAndDestroyRustString(resultPtr); + return JsonSerializer.Deserialize(result, CaseInsensitiveOptions)!; + } - return result; - } - } - catch (RustSdkException) + public unsafe string GenerateUserOrganizationKey(string userKey, string orgKey) + { + var userKeyBytes = StringToRustString(userKey); + var orgKeyBytes = StringToRustString(orgKey); + + fixed (byte* userKeyPtr = userKeyBytes) + fixed (byte* orgKeyPtr = orgKeyBytes) { - throw; // Re-throw our custom exceptions - } - catch (Exception ex) - { - throw new RustSdkException($"Failed to hash password: {ex.Message}", ex); + var resultPtr = NativeMethods.generate_user_organization_key(userKeyPtr, orgKeyPtr); + + var result = TakeAndDestroyRustString(resultPtr); + + return result; } } + private static byte[] StringToRustString(string str) { return Encoding.UTF8.GetBytes(str + '\0'); diff --git a/util/RustSdk/RustSdkServiceFactory.cs b/util/RustSdk/RustSdkServiceFactory.cs index 5a1e44eb22..e19275aacb 100644 --- a/util/RustSdk/RustSdkServiceFactory.cs +++ b/util/RustSdk/RustSdkServiceFactory.cs @@ -5,15 +5,6 @@ /// public static class RustSdkServiceFactory { - /// - /// Creates a new instance of the Rust SDK service - /// - /// A new IRustSdkService instance - public static RustSdkService Create() - { - return new RustSdkService(); - } - /// /// Creates a singleton instance of the Rust SDK service (thread-safe) /// @@ -25,6 +16,6 @@ public static class RustSdkServiceFactory private static class SingletonHolder { - internal static readonly RustSdkService Instance = new RustSdkService(); + internal static readonly RustSdkService Instance = new(); } } diff --git a/util/RustSdk/rust/Cargo.lock b/util/RustSdk/rust/Cargo.lock index 85a819427b..1665f387c4 100644 --- a/util/RustSdk/rust/Cargo.lock +++ b/util/RustSdk/rust/Cargo.lock @@ -135,7 +135,7 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bitwarden-api-api" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "reqwest", "serde", @@ -149,7 +149,7 @@ dependencies = [ [[package]] name = "bitwarden-api-identity" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "reqwest", "serde", @@ -163,8 +163,9 @@ dependencies = [ [[package]] name = "bitwarden-core" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ + "async-trait", "base64", "bitwarden-api-api", "bitwarden-api-identity", @@ -181,10 +182,11 @@ dependencies = [ "rustls-platform-verifier", "schemars 0.8.22", "serde", + "serde_bytes", "serde_json", "serde_qs", "serde_repr", - "thiserror 2.0.12", + "thiserror 1.0.69", "uuid", "zeroize", ] @@ -192,7 +194,7 @@ dependencies = [ [[package]] name = "bitwarden-crypto" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "aes", "argon2", @@ -220,7 +222,7 @@ dependencies = [ "sha1", "sha2", "subtle", - "thiserror 2.0.12", + "thiserror 1.0.69", "typenum", "uuid", "zeroize", @@ -230,7 +232,7 @@ dependencies = [ [[package]] name = "bitwarden-error" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "bitwarden-error-macro", ] @@ -238,7 +240,7 @@ dependencies = [ [[package]] name = "bitwarden-error-macro" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "darling", "proc-macro2", @@ -249,16 +251,16 @@ dependencies = [ [[package]] name = "bitwarden-state" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "async-trait", - "thiserror 2.0.12", + "thiserror 1.0.69", ] [[package]] name = "bitwarden-uuid" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "bitwarden-uuid-macro", ] @@ -266,7 +268,7 @@ dependencies = [ [[package]] name = "bitwarden-uuid-macro" version = "1.0.0" -source = "git+https://github.com/bitwarden/sdk-internal.git?rev=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=29c6158636d50141788e41736d15f2f6c7bc7fa8#29c6158636d50141788e41736d15f2f6c7bc7fa8" dependencies = [ "quote", "syn", @@ -1894,6 +1896,7 @@ dependencies = [ name = "sdk" version = "0.1.0" dependencies = [ + "base64", "bitwarden-core", "bitwarden-crypto", "csbindgen", diff --git a/util/RustSdk/rust/Cargo.toml b/util/RustSdk/rust/Cargo.toml index 82e1c5777c..8a9c4378cb 100644 --- a/util/RustSdk/rust/Cargo.toml +++ b/util/RustSdk/rust/Cargo.toml @@ -12,8 +12,9 @@ repository = "https://github.com/bitwarden/server" crate-type = ["cdylib"] [dependencies] -bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } -bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } +base64 = "0.22.1" +bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "29c6158636d50141788e41736d15f2f6c7bc7fa8" } +bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "29c6158636d50141788e41736d15f2f6c7bc7fa8" } serde = "=1.0.219" serde_json = "=1.0.141" diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs index 27dc3b2c4c..58fbbde741 100644 --- a/util/RustSdk/rust/src/lib.rs +++ b/util/RustSdk/rust/src/lib.rs @@ -4,7 +4,12 @@ use std::{ num::NonZeroU32, }; -use bitwarden_crypto::{HashPurpose, Kdf, MasterKey}; +use base64::{engine::general_purpose::STANDARD, Engine}; + +use bitwarden_crypto::{ + AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose, Kdf, MasterKey, + SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, +}; #[no_mangle] pub extern "C" fn my_add(x: i32, y: i32) -> i32 { @@ -29,13 +34,14 @@ pub unsafe extern "C" fn generate_user_keys( .derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization) .unwrap(); let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap(); - let keys = user_key.make_key_pair().unwrap(); + let keypair = user_key.make_key_pair().unwrap(); let json = serde_json::json!({ "masterPasswordHash": master_password_hash, + "key": user_key.0.to_base64(), "encryptedUserKey": encrypted_user_key.to_string(), - "publicKey": keys.public.to_string(), - "privateKey": keys.private.to_string(), + "publicKey": keypair.public.to_string(), + "privateKey": keypair.private.to_string(), }) .to_string(); @@ -44,31 +50,51 @@ pub unsafe extern "C" fn generate_user_keys( result.into_raw() } -/// # Safety -/// -/// The `email` and `password` pointers must be valid null-terminated C strings. -/// Both pointers must be non-null and point to valid memory for the duration of the function call. #[no_mangle] -pub unsafe extern "C" fn hash_password( - email: *const c_char, - password: *const c_char, +pub unsafe extern "C" fn generate_organization_keys() -> *const c_char { + let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + + let key = UserKey::new(key); + let keypair = key.make_key_pair().expect("Failed to generate key pair"); + + let json = serde_json::json!({ + "key": key.0.to_base64(), + "publicKey": keypair.public.to_string(), + "privateKey": keypair.private.to_string(), + }) + .to_string(); + + let result = CString::new(json).unwrap(); + + result.into_raw() +} + +#[no_mangle] +pub unsafe extern "C" fn generate_user_organization_key( + user_public_key: *const c_char, + organization_key: *const c_char, ) -> *const c_char { - let kdf = Kdf::PBKDF2 { - iterations: NonZeroU32::new(600_000).unwrap(), - }; + let user_public_key = CStr::from_ptr(user_public_key).to_str().unwrap().to_owned(); + let organization_key = CStr::from_ptr(organization_key) + .to_str() + .unwrap() + .to_owned(); - let email = CStr::from_ptr(email).to_str().unwrap(); - let password = CStr::from_ptr(password).to_str().unwrap(); + let user_public_key = STANDARD.decode(user_public_key).unwrap(); + let organization_key = STANDARD.decode(organization_key).unwrap(); - let master_key = MasterKey::derive(password, email, &kdf).unwrap(); + let encapsulation_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(user_public_key)).unwrap(); - let res = master_key - .derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization) - .unwrap(); + let encrypted_key = UnsignedSharedKey::encapsulate_key_unsigned( + &SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(organization_key)).unwrap(), + &encapsulation_key, + ) + .unwrap(); - let res = CString::new(res).unwrap(); + let result = CString::new(encrypted_key.to_string()).unwrap(); - res.into_raw() + result.into_raw() } /// # Safety diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs index 5e5cb17419..6363829de5 100644 --- a/util/Seeder/Factories/OrganizationSeeder.cs +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -2,14 +2,19 @@ using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Models; +using Bit.RustSDK; namespace Bit.Seeder.Factories; public class OrganizationSeeder { - public static Organization CreateEnterprise(string name, string domain, int seats) + public static (Organization organization, string key) CreateEnterprise(string name, string domain, int seats) { - return new Organization + var nativeService = RustSdkServiceFactory.CreateSingleton(); + + var keys = nativeService.GenerateOrganizationKeys(); + + var organization = new Organization { Id = Guid.NewGuid(), Name = name, @@ -20,23 +25,28 @@ public class OrganizationSeeder // Currently hardcoded to the values from https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/client/test_accounts.rs. // TODO: These should be dynamically generated by the SDK. - PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB", - PrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=", + PublicKey = keys.PublicKey, + PrivateKey = keys.PrivateKey, }; + + return (organization, keys.Key); } } -public static class OrgnaizationExtensions +public static class OrganizationExtensions { - public static OrganizationUser CreateOrganizationUser(this Organization organization, User user) + public static OrganizationUser CreateOrganizationUser(this Organization organization, User user, string orgKey) { + var nativeService = RustSdkServiceFactory.CreateSingleton(); + + var userOrgKey = nativeService.GenerateUserOrganizationKey(user.PublicKey!, orgKey); + return new OrganizationUser { Id = Guid.NewGuid(), OrganizationId = organization.Id, UserId = user.Id, - - Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Key = userOrgKey, Type = OrganizationUserType.Admin, Status = OrganizationUserStatusType.Confirmed }; diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index 80d6c13159..ec65bff085 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -9,7 +9,7 @@ namespace Bit.Seeder.Factories; public class UserSeeder { - public static User CreateUser(IPasswordHasher passwordHasher, string email) + public static (User user, string userKey) CreateUser(IPasswordHasher passwordHasher, string email) { var nativeService = RustSdkServiceFactory.CreateSingleton(); var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf"); @@ -31,6 +31,6 @@ public class UserSeeder user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash); - return user; + return (user, keys.Key); } } diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs index 776a32e5bb..e55d72ab83 100644 --- a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -11,28 +11,28 @@ public class OrganizationWithUsersRecipe(DatabaseContext db, IPasswordHasher(); var additionalOrgUsers = new List(); for (var i = 0; i < users; i++) { - var additionalUser = UserSeeder.CreateUser(passwordHasher, $"user{i}@{domain}"); + var (additionalUser, _) = UserSeeder.CreateUser(passwordHasher, $"user{i}@{domain}"); additionalUsers.Add(additionalUser); - additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); + additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser, orgKey)); } - //db.Add(organization); + db.Add(organization); db.Add(user); - //db.Add(orgUser); + db.Add(orgUser); db.SaveChanges(); // Use LinqToDB's BulkCopy for significant better performance - //db.BulkCopy(additionalUsers); - //db.BulkCopy(additionalOrgUsers); + db.BulkCopy(additionalUsers); + db.BulkCopy(additionalOrgUsers); return organization.Id; } From 3ad308138e78c75dbad21bce12ed8c861b7ca90a Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 31 Jul 2025 16:39:12 +0200 Subject: [PATCH 05/22] Speed up KDF --- .../OrganizationUsersControllerPerformanceTests.cs | 6 +++--- util/DbSeederUtility/Program.cs | 2 +- util/RustSdk/RustSdk.csproj | 8 ++++---- util/RustSdk/rust/Cargo.toml | 11 +++++++++++ util/RustSdk/rust/src/lib.rs | 7 +------ util/Seeder/Factories/UserSeeder.cs | 5 ++--- util/Seeder/Recipes/OrganizationWithUsersRecipe.cs | 3 +-- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs index 4c701344a4..95570b7018 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; -using Bit.Infrastructure.EntityFramework.Models; +using Bit.Core.Entities; using Bit.Seeder.Recipes; using Microsoft.AspNetCore.Identity; using Xunit; @@ -11,7 +11,7 @@ namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) { - [Theory()] + [Theory(Skip = "Performance test")] [InlineData(100)] [InlineData(60000)] public async Task GetAsync(int seats) @@ -25,7 +25,7 @@ public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOu var orgId = seeder.Seed("Org", seats, "large.test"); - var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="); + var tokens = await factory.LoginAsync("admin@large.test", "bSHqHVEoRiTtaKuHNQv7R3NR1RulppzZrwMO7E2YsEI="); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs index f6cadb65b1..2d9fc0a9ac 100644 --- a/util/DbSeederUtility/Program.cs +++ b/util/DbSeederUtility/Program.cs @@ -1,4 +1,4 @@ -using Bit.Infrastructure.EntityFramework.Models; +using Bit.Core.Entities; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Recipes; using CommandDotNet; diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj index c4d52a6630..a5b3c9385d 100644 --- a/util/RustSdk/RustSdk.csproj +++ b/util/RustSdk/RustSdk.csproj @@ -10,17 +10,17 @@ - + Always true runtimes/osx-arm64/native/libsdk.dylib - + true - + true - \ No newline at end of file + diff --git a/util/RustSdk/rust/Cargo.toml b/util/RustSdk/rust/Cargo.toml index 8a9c4378cb..47758cee4b 100644 --- a/util/RustSdk/rust/Cargo.toml +++ b/util/RustSdk/rust/Cargo.toml @@ -20,3 +20,14 @@ serde_json = "=1.0.141" [build-dependencies] csbindgen = "=1.9.3" + +# Compile all dependencies with some optimizations when building this crate on debug +# This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster +# As clean builds won't occur very often, this won't slow down the development process +[profile.dev.package."*"] +opt-level = 2 + +[profile.release] +codegen-units = 1 +lto = true +opt-level = 3 diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs index 58fbbde741..63b2541f77 100644 --- a/util/RustSdk/rust/src/lib.rs +++ b/util/RustSdk/rust/src/lib.rs @@ -11,11 +11,6 @@ use bitwarden_crypto::{ SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, }; -#[no_mangle] -pub extern "C" fn my_add(x: i32, y: i32) -> i32 { - x + y -} - #[no_mangle] pub unsafe extern "C" fn generate_user_keys( email: *const c_char, @@ -23,7 +18,7 @@ pub unsafe extern "C" fn generate_user_keys( ) -> *const c_char { // TODO: We might want to make KDF configurable in the future. let kdf = Kdf::PBKDF2 { - iterations: NonZeroU32::new(600_000).unwrap(), + iterations: NonZeroU32::new(5_000).unwrap(), }; let email = CStr::from_ptr(email).to_str().unwrap(); diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index ec65bff085..d6f3ec1762 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -1,5 +1,4 @@ using Bit.Core.Enums; -using Bit.Core.Services; using Bit.Infrastructure.EntityFramework.Models; using Bit.RustSDK; using Microsoft.AspNetCore.Identity; @@ -9,7 +8,7 @@ namespace Bit.Seeder.Factories; public class UserSeeder { - public static (User user, string userKey) CreateUser(IPasswordHasher passwordHasher, string email) + public static (User user, string userKey) CreateUser(IPasswordHasher passwordHasher, string email) { var nativeService = RustSdkServiceFactory.CreateSingleton(); var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf"); @@ -26,7 +25,7 @@ public class UserSeeder ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", Kdf = KdfType.PBKDF2_SHA256, - KdfIterations = 600_000, + KdfIterations = 5_000, }; user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash); diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs index e55d72ab83..c69bb893bb 100644 --- a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -1,5 +1,4 @@ -using Bit.Core.Services; -using Bit.Infrastructure.EntityFramework.Models; +using Bit.Core.Entities; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Factories; using LinqToDB.EntityFrameworkCore; From 9900a740311435fe2de07afa019f35e4f7e10014 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 1 Aug 2025 17:38:12 +0200 Subject: [PATCH 06/22] Use pre-generated RSA key --- ...nizationUsersControllerPerformanceTests.cs | 4 +- util/RustSdk/rust/src/lib.rs | 73 +++++++++++++++---- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs index 95570b7018..7ed81baa48 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -11,7 +11,7 @@ namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) { - [Theory(Skip = "Performance test")] + [Theory()] [InlineData(100)] [InlineData(60000)] public async Task GetAsync(int seats) @@ -25,7 +25,7 @@ public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOu var orgId = seeder.Seed("Org", seats, "large.test"); - var tokens = await factory.LoginAsync("admin@large.test", "bSHqHVEoRiTtaKuHNQv7R3NR1RulppzZrwMO7E2YsEI="); + var tokens = await factory.LoginAsync("admin@large.test", "z0Cvc58Q/lLhEDgtGnuPIFS/INOZP2qFlzaqzvTNEJI="); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs index 63b2541f77..7c7a628137 100644 --- a/util/RustSdk/rust/src/lib.rs +++ b/util/RustSdk/rust/src/lib.rs @@ -1,14 +1,12 @@ #![allow(clippy::missing_safety_doc)] -use std::{ - ffi::{c_char, CStr, CString}, - num::NonZeroU32, -}; +use std::ffi::{c_char, CStr, CString}; use base64::{engine::general_purpose::STANDARD, Engine}; use bitwarden_crypto::{ - AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose, Kdf, MasterKey, - SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + pbkdf2, AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose, + KeyEncryptable, MasterKey, RsaKeyPair, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, }; #[no_mangle] @@ -16,20 +14,18 @@ pub unsafe extern "C" fn generate_user_keys( email: *const c_char, password: *const c_char, ) -> *const c_char { - // TODO: We might want to make KDF configurable in the future. - let kdf = Kdf::PBKDF2 { - iterations: NonZeroU32::new(5_000).unwrap(), - }; - let email = CStr::from_ptr(email).to_str().unwrap(); let password = CStr::from_ptr(password).to_str().unwrap(); - let master_key = MasterKey::derive(password, email, &kdf).unwrap(); + let master_key = derive_master_key(password, email); + let master_password_hash = master_key .derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization) .unwrap(); + let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap(); - let keypair = user_key.make_key_pair().unwrap(); + + let keypair = keypair(&user_key.0); let json = serde_json::json!({ "masterPasswordHash": master_password_hash, @@ -45,6 +41,57 @@ pub unsafe extern "C" fn generate_user_keys( result.into_raw() } +fn derive_master_key(email: &str, password: &str) -> MasterKey { + let mut hash = pbkdf2(password.as_bytes(), email.as_bytes(), 5000); + + let hash = hash.as_mut_slice(); + + hash.try_into().unwrap() +} + +fn keypair(key: &SymmetricCryptoKey) -> RsaKeyPair { + const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS +8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2 +e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9 +4LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa +F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A +QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16 +l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq +92qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP +dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a +PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX +UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR +buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2 +hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc +vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA +hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs +tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY +UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs +zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX +E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP +jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez +MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9 +JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg +AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp +Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3 +WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz +XKZBokBGnjFnTnKcs7nv/O8= +-----END PRIVATE KEY-----"; + + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let public_key = private_key.to_public_key().to_der().unwrap(); + + let b64 = STANDARD.encode(public_key); + + let p = private_key.to_der().unwrap(); + + RsaKeyPair { + private: p.encrypt_with_key(key).unwrap(), + public: b64, + } +} + #[no_mangle] pub unsafe extern "C" fn generate_organization_keys() -> *const c_char { let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); From 7ceccafa7eea97c5a401c3af1327ce9ac837e990 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:37:07 -0400 Subject: [PATCH 07/22] Add license regression tests with frozen versions (#6408) --- .../Business/OrganizationLicenseTests.cs | 220 +++++++++++++++++- .../Models/Business/UserLicenseTests.cs | 168 +++++++++++++ 2 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 test/Core.Test/Billing/Models/Business/UserLicenseTests.cs diff --git a/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs index e6767a5be5..b2e94967ce 100644 --- a/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs +++ b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs @@ -1,15 +1,20 @@ using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Models.Business; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; +using Stripe; using Xunit; namespace Bit.Core.Test.Billing.Models.Business; public class OrganizationLicenseTests { - /// /// Verifies that when the license file is loaded from disk using the current OrganizationLicense class, /// it matches the Organization it was generated for. @@ -33,4 +38,217 @@ public class OrganizationLicenseTests }); Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings)); } + + /// + /// Known good GetDataBytes output for hash data (forHash: true) for all OrganizationLicense versions. + /// These values were verified to be correct on initial implementation and serve as regression baselines. + /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version. + /// + private static readonly Dictionary _knownGoodOrganizationLicenseHashData = new() + { + { 1, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UseTotp:true|Version:1" }, + { 2, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:2" }, + { 3, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:3" }, + { 4, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:4" }, + { 5, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:5" }, + { 6, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseTotp:true|Version:6" }, + { 7, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:7" }, + { 8, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:8" }, + { 9, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:9" }, + { 10, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:10" }, + { 11, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:11" }, + { 12, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:12" }, + { 13, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:13" }, + { 14, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:14" }, + { 15, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:15" }, + { 16, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:16" } + }; + + /// + /// Known good GetDataBytes output for signature data (forHash: false) for all OrganizationLicense versions. + /// These values were verified to be correct on initial implementation and serve as regression baselines. + /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version. + /// + private static readonly Dictionary _knownGoodOrganizationLicenseSignatureData = new() + { + { 1, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:WSyM/Q+vgOuWeF6XBH+RSfUqvf7NDtP3fgNfcbXYqKc=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UseTotp:true|Version:1" }, + { 2, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:n4g3leUf/egbnKk+/VgkJTvdxw2YRH6/zGgx89h+J60=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:2" }, + { 3, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:zDoMNV/c8YpUypc+FmBoPyj73qOsg4snsMOJDcKFp9k=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:3" }, + { 4, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:Y2sP9phSZ9GqbCC+PMp1KdnUhjfNaqNg6uzfUydrKZM=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:4" }, + { 5, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:PudZKNV7YAWJogm8BJf3wZIL+lESf3qzV/pQlZPPJjY=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:5" }, + { 6, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:7SjSYQENeAW4pUnXtsPaux2uipIWNWJz9VIrNW2gVsI=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseTotp:true|Version:6" }, + { 7, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:ujf4/zlDXv1g6ktlk9XBj/u3BkRZG+p5I00piGDiWp8=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:7" }, + { 8, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:GEM3AyWbQknnlDtoxyhw0QK7edYS2C/bffX5+p4G9ig=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:8" }, + { 9, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:5SF14wtEieiA9hjj+BTcrggHcx7dLEGbH+HLksvK79o=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:9" }, + { 10, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:NmbIpfiZUNxSvwbaolbUmItQCHIcVCTjfraR/NBlmvE=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:10" }, + { 11, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:dGZBQT/PORsuT/W2oRrngcjTboTyfZZVpDZBHshVK6Y=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:11" }, + { 12, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:rWecCXB0kuqi/RW3C8u2rLZRDMR49W3W4Q3eL2tZ3j8=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:12" }, + { 13, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:15fwM5v5Ba+t7JlD4ToYvtZmAoShWC3DrOD0lM5kXGE=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:13" }, + { 14, "license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:2bTNBiH2G/Nzv6UVD1BNJQBGjT9et0UO8ComQofS8uo=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:14" }, + { 15, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:3VjOyWJu38N4epIzhDzjRR80zQ651wnYkQCd+DIzeAs=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:15" }, + { 16, "license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:Oo5KFBoX8pMcklJ4oJAqgv77/WA8+gDPxq6+/Fjffwc=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:16" } + }; + + /// + /// Regression test that verifies GetDataBytes output for hash data (forHash: true) remains stable across all OrganizationLicense versions. + /// This protects against accidental changes to the data format that would break backward compatibility. + /// If this test fails, it means the hash data format has changed and existing licenses may no longer validate. + /// + [Fact] + public void OrganizationLicense_GetDataBytes_HashData_AllVersions() + { + // Verify each version produces the expected hash data format + for (var version = 1; version <= 16; version++) + { + var license = CreateDeterministicOrganizationLicense(version); + var actualHashData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: true)); + Assert.Equal(_knownGoodOrganizationLicenseHashData[version], actualHashData); + } + } + + /// + /// Regression test that verifies GetDataBytes output for signature data (forHash: false) remains stable across all OrganizationLicense versions. + /// This protects against accidental changes to the data format that would break backward compatibility. + /// If this test fails, it means the signature data format has changed and existing licenses may no longer validate. + /// + [Fact] + public void OrganizationLicense_GetDataBytes_SignatureData_AllVersions() + { + // Verify each version produces the expected signature data format + for (var version = 1; version <= 16; version++) + { + var license = CreateDeterministicOrganizationLicense(version); + var actualSignatureData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: false)); + Assert.Equal(_knownGoodOrganizationLicenseSignatureData[version], actualSignatureData); + } + } + + /// + /// Validates that the OrganizationLicense version remains frozen at version 15. + /// License versions should no longer be incremented. Use the JWT Token property to add new claims instead. + /// If this test fails, it means someone attempted to increment the license version, which is no longer allowed. + /// + [Fact] + public void OrganizationLicense_CurrentVersion_ShouldRemainFrozen() + { + const int expectedVersion = 15; + var actualVersion = OrganizationLicense.CurrentLicenseFileVersion; + + Assert.True(actualVersion == expectedVersion, $@" +ERROR: OrganizationLicense.CurrentLicenseFileVersion has been changed from {expectedVersion} to {actualVersion} + +License versions are now frozen and should not be incremented. + +Instead of incrementing the version: +- Use the JWT Token property to add new claims +- Add your new capabilities as claims in the Token +- This allows for more flexible licensing without breaking backward compatibility + +If you believe you need to change the version for a valid reason, please discuss with the team first. +"); + } + + /// + /// Creates a deterministic OrganizationLicense for testing hash values. + /// All property values are fixed to ensure reproducible hashes. + /// + private static OrganizationLicense CreateDeterministicOrganizationLicense(int version) + { + var organization = CreateDeterministicOrganization(); + var subscriptionInfo = CreateDeterministicSubscriptionInfo(); + var installationId = new Guid("78900000-0000-0000-0000-000000000123"); + var mockLicensingService = CreateMockLicensingService(); + + var license = new OrganizationLicense(organization, subscriptionInfo, installationId, mockLicensingService, version); + + // Override timestamps to deterministic values (constructor sets them to DateTime.UtcNow) + license.Issued = new DateTime(2025, 9, 26, 12, 0, 1, DateTimeKind.Utc); // Corresponds to 1759501361 Unix timestamp + license.Refresh = new DateTime(2025, 10, 26, 12, 0, 1, DateTimeKind.Utc); // Corresponds to 1762093361 Unix timestamp + + // Recalculate hash with the deterministic Issued/Refresh values + license.Hash = Convert.ToBase64String(license.ComputeHash()); + license.Signature = Convert.ToBase64String(mockLicensingService.SignLicense(license)); + + return license; + } + + /// + /// Creates an Organization with deterministic property values for reproducible testing. + /// + private static Organization CreateDeterministicOrganization() + { + return new Organization + { + Id = new Guid("12300000-0000-0000-0000-000000000456"), + Identifier = "myIdentifier", + Name = "myOrg", + BillingEmail = "myBillingEmail", + Plan = "myPlan", + PlanType = PlanType.EnterpriseAnnually2020, + Seats = 10, + MaxCollections = 2, + UsePolicies = true, + UseSso = true, + UseKeyConnector = true, + UseScim = true, + UseGroups = true, + UseEvents = true, + UseDirectory = true, + UseTotp = true, + Use2fa = true, + UseApi = true, + UseResetPassword = true, + MaxStorageGb = 100, + SelfHost = true, + UsersGetPremium = true, + UseCustomPermissions = true, + Enabled = true, + LicenseKey = "myLicenseKey", + UsePasswordManager = true, + UseSecretsManager = true, + SmSeats = 5, + SmServiceAccounts = 8, + UseRiskInsights = false, + LimitCollectionCreation = true, + LimitCollectionDeletion = true, + AllowAdminAccessToAllCollectionItems = true, + UseOrganizationDomains = true, + UseAdminSponsoredFamilies = false + }; + } + + /// + /// Creates a SubscriptionInfo with deterministic dates for reproducible testing. + /// + private static SubscriptionInfo CreateDeterministicSubscriptionInfo() + { + var stripeSubscription = new Subscription + { + Status = "active", + TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc), + CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + }; + + return new SubscriptionInfo + { + UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice + { + Date = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + }, + Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription) + }; + } + + /// + /// Creates a mock ILicensingService that returns a deterministic signature. + /// + private static ILicensingService CreateMockLicensingService() + { + var mockService = Substitute.For(); + mockService.SignLicense(Arg.Any()) + .Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing + return mockService; + } } diff --git a/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs b/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs new file mode 100644 index 0000000000..2d1e21b8c5 --- /dev/null +++ b/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs @@ -0,0 +1,168 @@ +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Models.Business; +using NSubstitute; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Models.Business; + +public class UserLicenseTests +{ + /// + /// Known good GetDataBytes output for hash data (forHash: true) for UserLicense version 1. + /// This value was verified to be correct on initial implementation and serves as a regression baseline. + /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version. + /// + private const string _knownGoodUserLicenseHashData = "license:user|Email:test@example.com|Expires:1736208000|Id:12300000-0000-0000-0000-000000000789|LicenseKey:myUserLicenseKey|MaxStorageGb:10|Name:Test User|Premium:true|Trial:false|Version:1"; + + /// + /// Known good GetDataBytes output for signature data (forHash: false) for UserLicense version 1. + /// This value was verified to be correct on initial implementation and serves as a regression baseline. + /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version. + /// + private const string _knownGoodUserLicenseSignatureData = "license:user|Email:test@example.com|Expires:1736208000|Hash:oZEopNmWvWQNE3Lnsh/LP2OPo6+IHxjTcpdIse/viQk=|Id:12300000-0000-0000-0000-000000000789|Issued:1758888041|LicenseKey:myUserLicenseKey|MaxStorageGb:10|Name:Test User|Premium:true|Refresh:1735603200|Trial:false|Version:1"; + + /// + /// Regression test that verifies GetDataBytes output for hash data (forHash: true) remains stable for UserLicense version 1. + /// This protects against accidental changes to the data format that would break backward compatibility. + /// If this test fails, it means the hash data format has changed and existing licenses may no longer validate. + /// + [Fact] + public void UserLicense_GetDataBytes_HashData_Version1() + { + var license = CreateDeterministicUserLicense(); + var actualHashData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: true)); + Assert.Equal(_knownGoodUserLicenseHashData, actualHashData); + } + + /// + /// Regression test that verifies GetDataBytes output for signature data (forHash: false) remains stable for UserLicense version 1. + /// This protects against accidental changes to the data format that would break backward compatibility. + /// If this test fails, it means the signature data format has changed and existing licenses may no longer validate. + /// + [Fact] + public void UserLicense_GetDataBytes_SignatureData_Version1() + { + var license = CreateDeterministicUserLicense(); + var actualSignatureData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: false)); + Assert.Equal(_knownGoodUserLicenseSignatureData, actualSignatureData); + } + + /// + /// Validates that the UserLicense version remains frozen at version 1. + /// License versions should no longer be incremented. Use the JWT Token property to add new claims instead. + /// If this test fails, it means someone attempted to add version 2 support, which is no longer allowed. + /// + [Fact] + public void UserLicense_CurrentVersion_ShouldRemainFrozen() + { + const int expectedMaxVersion = 1; + + var user = CreateDeterministicUser(); + var subscriptionInfo = CreateDeterministicSubscriptionInfo(); + var mockLicensingService = CreateMockLicensingService(); + + // Verify that version 2 is NOT supported (should throw NotSupportedException) + var exception = Assert.Throws(() => + new UserLicense(user, subscriptionInfo, mockLicensingService, version: 2)); + + // If the exception message changes or we don't get an exception, fail with helpful guidance + if (exception == null) + { + var errorMessage = $@" +ERROR: UserLicense now supports version 2 or higher + +License versions are now frozen and should not be incremented. + +Instead of incrementing the version: +- Use the JWT Token property to add new claims +- Add your new capabilities as claims in the Token +- This allows for more flexible licensing without breaking backward compatibility + +If you believe you need to change the version for a valid reason, please discuss with the team first. +"; + Assert.Fail(errorMessage); + } + + // Verify we still support version 1 + var license = new UserLicense(user, subscriptionInfo, mockLicensingService, version: expectedMaxVersion); + Assert.NotNull(license); + } + + /// + /// Creates a deterministic UserLicense for testing hash values. + /// All property values are fixed to ensure reproducible hashes. + /// + private static UserLicense CreateDeterministicUserLicense() + { + var user = CreateDeterministicUser(); + var subscriptionInfo = CreateDeterministicSubscriptionInfo(); + var mockLicensingService = CreateMockLicensingService(); + + var license = new UserLicense(user, subscriptionInfo, mockLicensingService, version: 1); + + // Override timestamps to deterministic values (constructor sets them to DateTime.UtcNow) + license.Issued = new DateTime(2025, 9, 26, 12, 0, 41, DateTimeKind.Utc); // Corresponds to 1759502041 Unix timestamp + license.Refresh = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc); // Corresponds to 1735603200 Unix timestamp + + // Recalculate hash with the deterministic Issued/Refresh values + license.Hash = Convert.ToBase64String(license.ComputeHash()); + license.Signature = Convert.ToBase64String(mockLicensingService.SignLicense(license)); + + return license; + } + + /// + /// Creates a User with deterministic property values for reproducible testing. + /// + private static User CreateDeterministicUser() + { + return new User + { + Id = new Guid("12300000-0000-0000-0000-000000000789"), + Name = "Test User", + Email = "test@example.com", + LicenseKey = "myUserLicenseKey", + Premium = true, + MaxStorageGb = 10, + PremiumExpirationDate = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + }; + } + + /// + /// Creates a SubscriptionInfo with deterministic dates for reproducible testing. + /// + private static SubscriptionInfo CreateDeterministicSubscriptionInfo() + { + var stripeSubscription = new Subscription + { + Status = "active", + TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc), + CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + }; + + return new SubscriptionInfo + { + UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice + { + Date = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + }, + Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription) + }; + } + + /// + /// Creates a mock ILicensingService that returns a deterministic signature. + /// + private static ILicensingService CreateMockLicensingService() + { + var mockService = Substitute.For(); + mockService.SignLicense(Arg.Any()) + .Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing + return mockService; + } +} From 6834cf72bab43664c1d772c83bc26e201e25d079 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:42:46 -0700 Subject: [PATCH 08/22] remove feature flag (#5979) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d4348e5093..a0596c067d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -234,7 +234,6 @@ public static class FeatureFlagKeys /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; - public const string SecurityTasks = "security-tasks"; public const string CipherKeyEncryption = "cipher-key-encryption"; public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; From 474e3f163c006aec935fc908ffc02ecf882bfefd Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 8 Oct 2025 06:59:43 +1000 Subject: [PATCH 09/22] Fix MariaDB support for development (#6420) - fix volume error when starting Docker container - fix overlapping indexes of integration test databases - add ef profile to start all EF containers at once --- dev/docker-compose.yml | 5 ++++- dev/migrate.ps1 | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index a88d877f77..c5e42cf9e3 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -53,6 +53,7 @@ services: - ./.data/postgres/log:/var/log/postgresql profiles: - postgres + - ef mysql: image: mysql:8.0 @@ -69,6 +70,7 @@ services: - mysql_dev_data:/var/lib/mysql profiles: - mysql + - ef mariadb: image: mariadb:10 @@ -76,13 +78,13 @@ services: - 4306:3306 environment: MARIADB_USER: maria - MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD} MARIADB_DATABASE: vault_dev MARIADB_RANDOM_ROOT_PASSWORD: "true" volumes: - mariadb_dev_data:/var/lib/mysql profiles: - mariadb + - ef idp: image: kenchan0130/simplesamlphp:1.19.8 @@ -153,5 +155,6 @@ volumes: mssql_dev_data: postgres_dev_data: mysql_dev_data: + mariadb_dev_data: rabbitmq_data: redis_data: diff --git a/dev/migrate.ps1 b/dev/migrate.ps1 index 287a2d18ee..26caa87efd 100755 --- a/dev/migrate.ps1 +++ b/dev/migrate.ps1 @@ -70,7 +70,7 @@ Foreach ($item in @( @($mysql, "MySQL", "MySqlMigrations", "mySql", 2), # MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context. # However they can still be run independently for integration tests. - @($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3) + @($mariadb, "MariaDB", "MySqlMigrations", "mySql", 4) )) { if (!$item[0] -and !$all) { continue From 876a2133b35fde97899adfab10bc66ece25edc4f Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:08:02 -0500 Subject: [PATCH 10/22] [PM-23713] new feature flag for premium badge interaction (#6425) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a0596c067d..0a85da6868 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -185,6 +185,7 @@ public static class FeatureFlagKeys public const string PM23385_UseNewPremiumFlow = "pm-23385-use-new-premium-flow"; public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; + public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; /* Key Management Team */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; From 98210b5e9c0b07a435ef8ef72a1cd390e59f47f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:44:07 +0200 Subject: [PATCH 11/22] [deps]: Update actions/github-script action to v8 (#6332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30fcf29206..fe82f9fbe6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -484,7 +484,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Trigger self-host build - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} script: | @@ -525,7 +525,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Trigger k8s deploy - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} script: | From 0e1edadeb7f38ba516788b65f239325a4f7eb389 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:15:01 -0500 Subject: [PATCH 12/22] chore: remove sso details feature flag reference, refs PM-21472 (#6422) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0a85da6868..a40c244279 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -135,7 +135,6 @@ public static class AuthenticationSchemes public static class FeatureFlagKeys { /* Admin Console Team */ - public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string PolicyRequirements = "pm-14439-policy-requirements"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; From e191ae965110bfe0a996b1977a8d67a7eff59762 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:21:23 -0500 Subject: [PATCH 13/22] consolidating 2 feature flags into one (#6430) it was determined we don't need the one that was removed --- .../Billing/Controllers/VNext/AccountBillingVNextController.cs | 2 +- .../Controllers/VNext/SelfHostedAccountBillingController.cs | 2 +- src/Core/Constants.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs index 97f2003d29..b01b629e4f 100644 --- a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs @@ -66,7 +66,7 @@ public class AccountBillingVNextController( } [HttpPost("subscription")] - [RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)] + [RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)] [InjectUser] public async Task CreateSubscriptionAsync( [BindNever] User user, diff --git a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs index 544753ad0f..973a7d99a1 100644 --- a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs +++ b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs @@ -21,7 +21,7 @@ public class SelfHostedAccountBillingController( ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController { [HttpPost("license")] - [RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)] + [RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)] [InjectUser] public async Task UploadLicenseAsync( [BindNever] User user, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a40c244279..cbe1b04bda 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -181,7 +181,6 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; - public const string PM23385_UseNewPremiumFlow = "pm-23385-use-new-premium-flow"; public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; From 14aa450e7c2cd691d9e4a4dda6fca27b65015de3 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:04:02 -0500 Subject: [PATCH 14/22] chore: remove limit item deletion feature flag reference, refs PM-17155 (#6423) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cbe1b04bda..1574c7f2ce 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -135,7 +135,6 @@ public static class AuthenticationSchemes public static class FeatureFlagKeys { /* Admin Console Team */ - public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string PolicyRequirements = "pm-14439-policy-requirements"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; From 0fbc314fb4950c77529d6be6cca686d53160852e Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:21:02 -0500 Subject: [PATCH 15/22] [PM-24954] [PM-24955] Remove BulkResourceCreationService Feature Flag (#6428) * chore: remove ff implementation and superflous tests, refs PM-24954 * chore: remove UpdateForKeyRotation vNext, refs PM-24954 * chore: remove CreateAsync vNext for ImportCiphersCommand, refs PM-24954 * chore: remove UpdateCiphersAsync vNext from CipherService, refs PM-24954 * chore: formatting, refs PM-24954 * chore: fix CipherRepositoryTests by removing vNext references, refs PM-24954 * chore: remove bulk cipher copy ff key, refs PM-24954 --- src/Core/Constants.cs | 1 - .../RotateUserAccountkeysCommand.cs | 12 +- .../ImportFeatures/ImportCiphersCommand.cs | 20 +- .../Vault/Repositories/ICipherRepository.cs | 22 -- .../Services/Implementations/CipherService.cs | 10 +- .../Vault/Repositories/CipherRepository.cs | 257 ------------------ .../Vault/Repositories/CipherRepository.cs | 41 --- .../ImportCiphersAsyncCommandTests.cs | 128 --------- .../Vault/Services/CipherServiceTests.cs | 53 ---- .../Repositories/CipherRepositoryTests.cs | 41 +-- 10 files changed, 10 insertions(+), 575 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1574c7f2ce..80b74877c5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -141,7 +141,6 @@ public static class FeatureFlagKeys public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; - public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs index 011fc2932f..91363abee8 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs @@ -25,7 +25,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IWebAuthnCredentialRepository _credentialRepository; private readonly IPasswordHasher _passwordHasher; - private readonly IFeatureService _featureService; /// /// Instantiates a new @@ -61,7 +60,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand _identityErrorDescriber = errors; _credentialRepository = credentialRepository; _passwordHasher = passwordHasher; - _featureService = featureService; } /// @@ -103,15 +101,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand List saveEncryptedDataActions = new(); if (model.Ciphers.Any()) { - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation_vNext(user.Id, model.Ciphers)); - } - else - { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); - } + saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); } if (model.Folders.Any()) diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index ce269bc68c..c7f7e3aff7 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -108,15 +108,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - await _cipherRepository.CreateAsync_vNext(importingUserId, ciphers, newFolders); - } - else - { - await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); - } + await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); // push await _pushService.PushSyncVaultAsync(importingUserId); @@ -191,15 +183,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - await _cipherRepository.CreateAsync_vNext(ciphers, newCollections, collectionCiphers, newCollectionUsers); - } - else - { - await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); - } + await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); // push await _pushService.PushSyncVaultAsync(importingUserId); diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 32acf3cbc9..94518bae2a 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -33,28 +33,12 @@ public interface ICipherRepository : IRepository Task DeleteByUserIdAsync(Guid userId); Task DeleteByOrganizationIdAsync(Guid organizationId); Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers); - /// - /// - /// This version uses the bulk resource creation service to create the temp table. - /// - Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers); /// /// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items. /// Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders); - /// - /// - /// This version uses the bulk resource creation service to create the temp tables. - /// - Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, IEnumerable collectionUsers); - /// - /// - /// This version uses the bulk resource creation service to create the temp tables. - /// - Task CreateAsync_vNext(IEnumerable ciphers, IEnumerable collections, - IEnumerable collectionCiphers, IEnumerable collectionUsers); Task SoftDeleteAsync(IEnumerable ids, Guid userId); Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); Task UnarchiveAsync(IEnumerable ids, Guid userId); @@ -92,10 +76,4 @@ public interface ICipherRepository : IRepository /// Task> GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId); - /// - /// - /// This version uses the bulk resource creation service to create the temp table. - /// - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext(Guid userId, - IEnumerable ciphers); } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index ca6bacd55a..f132588e37 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -644,15 +644,7 @@ public class CipherService : ICipherService cipherIds.Add(cipher.Id); } - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - await _cipherRepository.UpdateCiphersAsync_vNext(sharingUserId, cipherInfos.Select(c => c.cipher)); - } - else - { - await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher)); - } + await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher)); await _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId, organizationId, collectionIds); diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 4904574eee..48232ef484 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -13,7 +13,6 @@ using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.Dapper.AdminConsole.Helpers; using Bit.Infrastructure.Dapper.Repositories; -using Bit.Infrastructure.Dapper.Vault.Helpers; using Dapper; using Microsoft.Data.SqlClient; @@ -383,63 +382,6 @@ public class CipherRepository : Repository, ICipherRepository cmd.ExecuteNonQuery(); } - // Bulk copy data into temp table - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "#TempCipher"; - var ciphersTable = ciphers.ToDataTable(); - foreach (DataColumn col in ciphersTable.Columns) - { - bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); - } - - ciphersTable.PrimaryKey = new DataColumn[] { ciphersTable.Columns[0] }; - await bulkCopy.WriteToServerAsync(ciphersTable); - } - - // Update cipher table from temp table - var sql = @" - UPDATE - [dbo].[Cipher] - SET - [Data] = TC.[Data], - [Attachments] = TC.[Attachments], - [RevisionDate] = TC.[RevisionDate], - [Key] = TC.[Key] - FROM - [dbo].[Cipher] C - INNER JOIN - #TempCipher TC ON C.Id = TC.Id - WHERE - C.[UserId] = @UserId - - DROP TABLE #TempCipher"; - - await using (var cmd = new SqlCommand(sql, connection, transaction)) - { - cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId; - cmd.ExecuteNonQuery(); - } - }; - } - - /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext( - Guid userId, IEnumerable ciphers) - { - return async (SqlConnection connection, SqlTransaction transaction) => - { - // Create temp table - var sqlCreateTemp = @" - SELECT TOP 0 * - INTO #TempCipher - FROM [dbo].[Cipher]"; - - await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) - { - cmd.ExecuteNonQuery(); - } - // Bulk copy data into temp table await BulkResourceCreationService.CreateTempCiphersAsync(connection, transaction, ciphers); @@ -476,88 +418,6 @@ public class CipherRepository : Repository, ICipherRepository return; } - using (var connection = new SqlConnection(ConnectionString)) - { - connection.Open(); - - using (var transaction = connection.BeginTransaction()) - { - try - { - // 1. Create temp tables to bulk copy into. - - var sqlCreateTemp = @" - SELECT TOP 0 * - INTO #TempCipher - FROM [dbo].[Cipher]"; - - using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) - { - cmd.ExecuteNonQuery(); - } - - // 2. Bulk copy into temp tables. - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "#TempCipher"; - var dataTable = BuildCiphersTable(bulkCopy, ciphers); - bulkCopy.WriteToServer(dataTable); - } - - // 3. Insert into real tables from temp tables and clean up. - - // Intentionally not including Favorites, Folders, and CreationDate - // since those are not meant to be bulk updated at this time - var sql = @" - UPDATE - [dbo].[Cipher] - SET - [UserId] = TC.[UserId], - [OrganizationId] = TC.[OrganizationId], - [Type] = TC.[Type], - [Data] = TC.[Data], - [Attachments] = TC.[Attachments], - [RevisionDate] = TC.[RevisionDate], - [DeletedDate] = TC.[DeletedDate], - [Key] = TC.[Key] - FROM - [dbo].[Cipher] C - INNER JOIN - #TempCipher TC ON C.Id = TC.Id - WHERE - C.[UserId] = @UserId - - DROP TABLE #TempCipher"; - - using (var cmd = new SqlCommand(sql, connection, transaction)) - { - cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId; - cmd.ExecuteNonQuery(); - } - - await connection.ExecuteAsync( - $"[{Schema}].[User_BumpAccountRevisionDate]", - new { Id = userId }, - commandType: CommandType.StoredProcedure, transaction: transaction); - - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - } - } - - public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers) - { - if (!ciphers.Any()) - { - return; - } - using (var connection = new SqlConnection(ConnectionString)) { connection.Open(); @@ -635,54 +495,6 @@ public class CipherRepository : Repository, ICipherRepository return; } - using (var connection = new SqlConnection(ConnectionString)) - { - connection.Open(); - - using (var transaction = connection.BeginTransaction()) - { - try - { - if (folders.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Folder]"; - var dataTable = BuildFoldersTable(bulkCopy, folders); - bulkCopy.WriteToServer(dataTable); - } - } - - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Cipher]"; - var dataTable = BuildCiphersTable(bulkCopy, ciphers); - bulkCopy.WriteToServer(dataTable); - } - - await connection.ExecuteAsync( - $"[{Schema}].[User_BumpAccountRevisionDate]", - new { Id = userId }, - commandType: CommandType.StoredProcedure, transaction: transaction); - - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - } - } - - public async Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, IEnumerable folders) - { - if (!ciphers.Any()) - { - return; - } - using (var connection = new SqlConnection(ConnectionString)) { connection.Open(); @@ -722,75 +534,6 @@ public class CipherRepository : Repository, ICipherRepository return; } - using (var connection = new SqlConnection(ConnectionString)) - { - connection.Open(); - - using (var transaction = connection.BeginTransaction()) - { - try - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Cipher]"; - var dataTable = BuildCiphersTable(bulkCopy, ciphers); - bulkCopy.WriteToServer(dataTable); - } - - if (collections.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Collection]"; - var dataTable = BuildCollectionsTable(bulkCopy, collections); - bulkCopy.WriteToServer(dataTable); - } - } - - if (collectionCiphers.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[CollectionCipher]"; - var dataTable = BuildCollectionCiphersTable(bulkCopy, collectionCiphers); - bulkCopy.WriteToServer(dataTable); - } - } - - if (collectionUsers.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[CollectionUser]"; - var dataTable = BuildCollectionUsersTable(bulkCopy, collectionUsers); - bulkCopy.WriteToServer(dataTable); - } - } - - await connection.ExecuteAsync( - $"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]", - new { OrganizationId = ciphers.First().OrganizationId }, - commandType: CommandType.StoredProcedure, transaction: transaction); - - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - } - } - - public async Task CreateAsync_vNext(IEnumerable ciphers, IEnumerable collections, - IEnumerable collectionCiphers, IEnumerable collectionUsers) - { - if (!ciphers.Any()) - { - return; - } - using (var connection = new SqlConnection(ConnectionString)) { connection.Open(); diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index d88f0e98bb..3c45afe530 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -168,16 +168,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular create method. - /// - public async Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, - IEnumerable folders) - { - await CreateAsync(userId, ciphers, folders); - } - public async Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, @@ -216,18 +206,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular create method. - /// - public async Task CreateAsync_vNext(IEnumerable ciphers, - IEnumerable collections, - IEnumerable collectionCiphers, - IEnumerable collectionUsers) - { - await CreateAsync(ciphers, collections, collectionCiphers, collectionUsers); - } - public async Task DeleteAsync(IEnumerable ids, Guid userId) { await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.HardDelete); @@ -986,15 +964,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular update method. - /// - public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers) - { - await UpdateCiphersAsync(userId, ciphers); - } - public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite) { using (var scope = ServiceScopeFactory.CreateScope()) @@ -1107,16 +1076,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular update method. - /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext( - Guid userId, IEnumerable ciphers) - { - return UpdateForKeyRotation(userId, ciphers); - } - public async Task UpsertAsync(CipherDetails cipher) { if (cipher.Id.Equals(default)) diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 11f637d207..8c1cb1d5ff 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -53,38 +53,6 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } - [Theory, BitAutoData] - public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_Success( - Guid importingUserId, - List ciphers, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership) - .Returns(false); - - sutProvider.GetDependency() - .GetManyByUserIdAsync(importingUserId) - .Returns(new List()); - - var folders = new List { new Folder { UserId = importingUserId } }; - - var folderRelationships = new List>(); - - // Act - await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .CreateAsync_vNext(importingUserId, ciphers, Arg.Any>()); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - } - [Theory, BitAutoData] public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success( Guid importingUserId, @@ -117,42 +85,6 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } - [Theory, BitAutoData] - public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success( - Guid importingUserId, - List ciphers, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyRequirements) - .Returns(true); - - sutProvider.GetDependency() - .GetAsync(importingUserId) - .Returns(new OrganizationDataOwnershipPolicyRequirement( - OrganizationDataOwnershipState.Disabled, - [])); - - sutProvider.GetDependency() - .GetManyByUserIdAsync(importingUserId) - .Returns(new List()); - - var folders = new List { new Folder { UserId = importingUserId } }; - - var folderRelationships = new List>(); - - await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - - await sutProvider.GetDependency() - .Received(1) - .CreateAsync_vNext(importingUserId, ciphers, Arg.Any>()); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - } - [Theory, BitAutoData] public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException( List folders, @@ -259,66 +191,6 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } - [Theory, BitAutoData] - public async Task ImportIntoOrganizationalVaultAsync_WithBulkResourceCreationServiceEnabled_Success( - Organization organization, - Guid importingUserId, - OrganizationUser importingOrganizationUser, - List collections, - List ciphers, - SutProvider sutProvider) - { - organization.MaxCollections = null; - importingOrganizationUser.OrganizationId = organization.Id; - - foreach (var collection in collections) - { - collection.OrganizationId = organization.Id; - } - - foreach (var cipher in ciphers) - { - cipher.OrganizationId = organization.Id; - } - - KeyValuePair[] collectionRelationships = { - new(0, 0), - new(1, 1), - new(2, 2) - }; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - - sutProvider.GetDependency() - .GetByOrganizationAsync(organization.Id, importingUserId) - .Returns(importingOrganizationUser); - - // Set up a collection that already exists in the organization - sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(organization.Id) - .Returns(new List { collections[0] }); - - await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId); - - await sutProvider.GetDependency().Received(1).CreateAsync_vNext( - ciphers, - Arg.Is>(cols => cols.Count() == collections.Count - 1 && - !cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added - cols.All(c => collections.Any(x => c.Name == x.Name))), - Arg.Is>(c => c.Count() == ciphers.Count), - Arg.Is>(cus => - cus.Count() == collections.Count - 1 && - !cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization - cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true))); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - } - [Theory, BitAutoData] public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException( Organization organization, diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 44c86389e3..55db5a9143 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -674,32 +674,6 @@ public class CipherServiceTests Arg.Is>(arg => !arg.Except(ciphers).Any())); } - [Theory] - [BitAutoData("")] - [BitAutoData("Correct Time")] - public async Task ShareManyAsync_CorrectRevisionDate_WithBulkResourceCreationServiceEnabled_Passes(string revisionDateString, - SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency().GetByIdAsync(organization.Id) - .Returns(new Organization - { - PlanType = PlanType.EnterpriseAnnually, - MaxStorageGb = 100 - }); - - var cipherInfos = ciphers.Select(c => (c, - string.IsNullOrEmpty(revisionDateString) ? null : (DateTime?)c.RevisionDate)); - var sharingUserId = ciphers.First().UserId.Value; - - await sutProvider.Sut.ShareManyAsync(cipherInfos, organization.Id, collectionIds, sharingUserId); - await sutProvider.GetDependency().Received(1).UpdateCiphersAsync_vNext(sharingUserId, - Arg.Is>(arg => !arg.Except(ciphers).Any())); - } - [Theory] [BitAutoData] public async Task RestoreAsync_UpdatesUserCipher(Guid restoringUserId, CipherDetails cipher, SutProvider sutProvider) @@ -1120,33 +1094,6 @@ public class CipherServiceTests Arg.Is>(arg => !arg.Except(ciphers).Any())); } - [Theory, BitAutoData] - public async Task ShareManyAsync_PaidOrgWithAttachment_WithBulkResourceCreationServiceEnabled_Passes(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency().GetByIdAsync(organizationId) - .Returns(new Organization - { - PlanType = PlanType.EnterpriseAnnually, - MaxStorageGb = 100 - }); - ciphers.FirstOrDefault().Attachments = - "{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\"," - + "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}"; - - var cipherInfos = ciphers.Select(c => (c, - (DateTime?)c.RevisionDate)); - var sharingUserId = ciphers.First().UserId.Value; - - await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId); - await sutProvider.GetDependency().Received(1).UpdateCiphersAsync_vNext(sharingUserId, - Arg.Is>(arg => !arg.Except(ciphers).Any())); - } - private class SaveDetailsAsyncDependencies { public CipherDetails CipherDetails { get; set; } diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index 3a44453ed6..bb53bb1fd9 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -949,36 +949,7 @@ public class CipherRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task UpdateCiphersAsync_Works(ICipherRepository cipherRepository, IUserRepository userRepository) - { - var user = await userRepository.CreateAsync(new User - { - Name = "Test User", - Email = $"test+{Guid.NewGuid()}@email.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); - - var cipher1 = await CreatePersonalCipher(user, cipherRepository); - var cipher2 = await CreatePersonalCipher(user, cipherRepository); - - cipher1.Type = CipherType.SecureNote; - cipher2.Attachments = "new_attachments"; - - await cipherRepository.UpdateCiphersAsync(user.Id, [cipher1, cipher2]); - - var updatedCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id); - var updatedCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id); - - Assert.NotNull(updatedCipher1); - Assert.NotNull(updatedCipher2); - - Assert.Equal(CipherType.SecureNote, updatedCipher1.Type); - Assert.Equal("new_attachments", updatedCipher2.Attachments); - } - - [DatabaseTheory, DatabaseData] - public async Task CreateAsync_vNext_WithFolders_Works( + public async Task CreateAsync_WithFolders_Works( IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository) { // Arrange @@ -996,7 +967,7 @@ public class CipherRepositoryTests var cipher2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.SecureNote, UserId = user.Id, Data = "" }; // Act - await cipherRepository.CreateAsync_vNext( + await cipherRepository.CreateAsync( userId: user.Id, ciphers: [cipher1, cipher2], folders: [folder1, folder2]); @@ -1014,7 +985,7 @@ public class CipherRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task CreateAsync_vNext_WithCollectionsAndUsers_Works( + public async Task CreateAsync_WithCollectionsAndUsers_Works( IOrganizationRepository orgRepository, IOrganizationUserRepository orgUserRepository, ICollectionRepository collectionRepository, @@ -1059,7 +1030,7 @@ public class CipherRepositoryTests }; // Act - await cipherRepository.CreateAsync_vNext( + await cipherRepository.CreateAsync( ciphers: [cipher], collections: [collection], collectionCiphers: [collectionCipher], @@ -1084,7 +1055,7 @@ public class CipherRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task UpdateCiphersAsync_vNext_Works( + public async Task UpdateCiphersAsync_Works( IUserRepository userRepository, ICipherRepository cipherRepository) { // Arrange @@ -1110,7 +1081,7 @@ public class CipherRepositoryTests c2.Attachments = expectedNewAttachments; // Act - await cipherRepository.UpdateCiphersAsync_vNext(user.Id, [c1, c2]); + await cipherRepository.UpdateCiphersAsync(user.Id, [c1, c2]); // Assert var updated1 = await cipherRepository.GetByIdAsync(c1.Id); From fe32e885c6fd5d2403a38bda77c4cff3950d9f35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:47:29 -0400 Subject: [PATCH 16/22] [deps] Auth: Update Sustainsys.Saml2.AspNetCore2 to 2.11.0 (#6207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/Sso.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/src/Sso/Sso.csproj b/bitwarden_license/src/Sso/Sso.csproj index 1b6b666ab1..2a1c14ae5a 100644 --- a/bitwarden_license/src/Sso/Sso.csproj +++ b/bitwarden_license/src/Sso/Sso.csproj @@ -10,7 +10,7 @@ - + From a6726d2e04b14b7cd35a8d55dcca83d7ab3c1719 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 9 Oct 2025 10:47:54 -0500 Subject: [PATCH 17/22] PM-26208 updated api endpoint (#6431) --- src/Billing/BillingSettings.cs | 9 +++ .../Controllers/FreshdeskController.cs | 80 +++++++++++-------- .../OnyxAnswerWithCitationRequestModel.cs | 51 ++++++++---- .../OnyxAnswerWithCitationResponseModel.cs | 33 -------- src/Billing/Models/OnyxResponseModel.cs | 15 ++++ src/Billing/appsettings.json | 8 +- .../Controllers/FreshdeskControllerTests.cs | 2 +- 7 files changed, 115 insertions(+), 83 deletions(-) delete mode 100644 src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs create mode 100644 src/Billing/Models/OnyxResponseModel.cs diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 32630e4a4a..3dc3e3e808 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -44,6 +44,15 @@ public class BillingSettings { public virtual string ApiKey { get; set; } public virtual string BaseUrl { get; set; } + public virtual string Path { get; set; } public virtual int PersonaId { get; set; } + public virtual bool UseAnswerWithCitationModels { get; set; } = true; + + public virtual SearchSettings SearchSettings { get; set; } = new SearchSettings(); + } + public class SearchSettings + { + public virtual string RunSearch { get; set; } = "auto"; // "always", "never", "auto" + public virtual bool RealTime { get; set; } = true; } } diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 66d4f47d92..38ed05cfdf 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Net.Http.Headers; using System.Reflection; using System.Text; @@ -35,7 +32,7 @@ public class FreshdeskController : Controller GlobalSettings globalSettings, IHttpClientFactory httpClientFactory) { - _billingSettings = billingSettings?.Value; + _billingSettings = billingSettings?.Value ?? throw new ArgumentNullException(nameof(billingSettings)); _userRepository = userRepository; _organizationRepository = organizationRepository; _logger = logger; @@ -101,7 +98,8 @@ public class FreshdeskController : Controller customFields[_billingSettings.FreshDesk.OrgFieldName] += $"\n{orgNote}"; } - var planName = GetAttribute(org.PlanType).Name.Split(" ").FirstOrDefault(); + var displayAttribute = GetAttribute(org.PlanType); + var planName = displayAttribute?.Name?.Split(" ").FirstOrDefault(); if (!string.IsNullOrWhiteSpace(planName)) { tags.Add(string.Format("Org: {0}", planName)); @@ -159,28 +157,22 @@ public class FreshdeskController : Controller return Ok(); } - // create the onyx `answer-with-citation` request - var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId); - var onyxRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) - { - Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")), - }; - var (_, onyxJsonResponse) = await CallOnyxApi(onyxRequest); + // Get response from Onyx AI + var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model); // the CallOnyxApi will return a null if we have an error response - if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg)) + if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg)) { _logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ", JsonSerializer.Serialize(model), - JsonSerializer.Serialize(onyxRequestModel), - JsonSerializer.Serialize(onyxJsonResponse)); + JsonSerializer.Serialize(onyxRequest), + JsonSerializer.Serialize(onyxResponse)); return Ok(); // return ok so we don't retry } // add the answer as a note to the ticket - await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId); + await AddAnswerNoteToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId); return Ok(); } @@ -206,27 +198,21 @@ public class FreshdeskController : Controller } // create the onyx `answer-with-citation` request - var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId); - var onyxRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) - { - Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")), - }; - var (_, onyxJsonResponse) = await CallOnyxApi(onyxRequest); + var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model); // the CallOnyxApi will return a null if we have an error response - if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg)) + if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg)) { _logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ", JsonSerializer.Serialize(model), - JsonSerializer.Serialize(onyxRequestModel), - JsonSerializer.Serialize(onyxJsonResponse)); + JsonSerializer.Serialize(onyxRequest), + JsonSerializer.Serialize(onyxResponse)); return Ok(); // return ok so we don't retry } // add the reply to the ticket - await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId); + await AddReplyToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId); return Ok(); } @@ -356,7 +342,32 @@ public class FreshdeskController : Controller return await CallFreshdeskApiAsync(request, retriedCount++); } - private async Task<(HttpResponseMessage, T)> CallOnyxApi(HttpRequestMessage request) + async Task<(OnyxRequestModel onyxRequest, OnyxResponseModel onyxResponse)> GetAnswerFromOnyx(FreshdeskOnyxAiWebhookModel model) + { + // TODO: remove the use of the deprecated answer-with-citation models after we are sure + if (_billingSettings.Onyx.UseAnswerWithCitationModels) + { + var onyxRequest = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx); + var onyxAnswerWithCitationRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) + { + Content = JsonContent.Create(onyxRequest, mediaType: new MediaTypeHeaderValue("application/json")), + }; + var onyxResponse = await CallOnyxApi(onyxAnswerWithCitationRequest); + return (onyxRequest, onyxResponse); + } + + var request = new OnyxSendMessageSimpleApiRequestModel(model.TicketDescriptionText, _billingSettings.Onyx); + var onyxSimpleRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("{0}{1}", _billingSettings.Onyx.BaseUrl, _billingSettings.Onyx.Path)) + { + Content = JsonContent.Create(request, mediaType: new MediaTypeHeaderValue("application/json")), + }; + var onyxSimpleResponse = await CallOnyxApi(onyxSimpleRequest); + return (request, onyxSimpleResponse); + } + + private async Task CallOnyxApi(HttpRequestMessage request) where T : class, new() { var httpClient = _httpClientFactory.CreateClient("OnyxApi"); var response = await httpClient.SendAsync(request); @@ -365,7 +376,7 @@ public class FreshdeskController : Controller { _logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}", response.StatusCode, JsonSerializer.Serialize(response)); - return (null, default); + return new T(); } var responseStr = await response.Content.ReadAsStringAsync(); var responseJson = JsonSerializer.Deserialize(responseStr, options: new JsonSerializerOptions @@ -373,11 +384,12 @@ public class FreshdeskController : Controller PropertyNameCaseInsensitive = true, }); - return (response, responseJson); + return responseJson ?? new T(); } - private TAttribute GetAttribute(Enum enumValue) where TAttribute : Attribute + private TAttribute? GetAttribute(Enum enumValue) where TAttribute : Attribute { - return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute(); + var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault(); + return memberInfo != null ? memberInfo.GetCustomAttribute() : null; } } diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index ba3b89e297..9a753be4bc 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -1,35 +1,58 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - - -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using static Bit.Billing.BillingSettings; namespace Bit.Billing.Models; -public class OnyxAnswerWithCitationRequestModel +public class OnyxRequestModel { - [JsonPropertyName("messages")] - public List Messages { get; set; } - [JsonPropertyName("persona_id")] public int PersonaId { get; set; } = 1; [JsonPropertyName("retrieval_options")] - public RetrievalOptions RetrievalOptions { get; set; } + public RetrievalOptions RetrievalOptions { get; set; } = new RetrievalOptions(); - public OnyxAnswerWithCitationRequestModel(string message, int personaId = 1) + public OnyxRequestModel(OnyxSettings onyxSettings) + { + PersonaId = onyxSettings.PersonaId; + RetrievalOptions.RunSearch = onyxSettings.SearchSettings.RunSearch; + RetrievalOptions.RealTime = onyxSettings.SearchSettings.RealTime; + } +} + +/// +/// This is used with the onyx endpoint /query/answer-with-citation +/// which has been deprecated. This can be removed once later +/// +public class OnyxAnswerWithCitationRequestModel : OnyxRequestModel +{ + [JsonPropertyName("messages")] + public List Messages { get; set; } = new List(); + + public OnyxAnswerWithCitationRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) { message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); Messages = new List() { new Message() { MessageText = message } }; - RetrievalOptions = new RetrievalOptions(); - PersonaId = personaId; + } +} + +/// +/// This is used with the onyx endpoint /chat/send-message-simple-api +/// +public class OnyxSendMessageSimpleApiRequestModel : OnyxRequestModel +{ + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + public OnyxSendMessageSimpleApiRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) + { + Message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); } } public class Message { [JsonPropertyName("message")] - public string MessageText { get; set; } + public string MessageText { get; set; } = string.Empty; [JsonPropertyName("sender")] public string Sender { get; set; } = "user"; diff --git a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs deleted file mode 100644 index 5f67cd51d2..0000000000 --- a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Text.Json.Serialization; - -namespace Bit.Billing.Models; - -public class OnyxAnswerWithCitationResponseModel -{ - [JsonPropertyName("answer")] - public string Answer { get; set; } - - [JsonPropertyName("rephrase")] - public string Rephrase { get; set; } - - [JsonPropertyName("citations")] - public List Citations { get; set; } - - [JsonPropertyName("llm_selected_doc_indices")] - public List LlmSelectedDocIndices { get; set; } - - [JsonPropertyName("error_msg")] - public string ErrorMsg { get; set; } -} - -public class Citation -{ - [JsonPropertyName("citation_num")] - public int CitationNum { get; set; } - - [JsonPropertyName("document_id")] - public string DocumentId { get; set; } -} diff --git a/src/Billing/Models/OnyxResponseModel.cs b/src/Billing/Models/OnyxResponseModel.cs new file mode 100644 index 0000000000..96fa134c40 --- /dev/null +++ b/src/Billing/Models/OnyxResponseModel.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models; + +public class OnyxResponseModel +{ + [JsonPropertyName("answer")] + public string Answer { get; set; } = string.Empty; + + [JsonPropertyName("answer_citationless")] + public string AnswerCitationless { get; set; } = string.Empty; + + [JsonPropertyName("error_msg")] + public string ErrorMsg { get; set; } = string.Empty; +} diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 0074b5aafe..6c90c22686 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -80,7 +80,13 @@ "onyx": { "apiKey": "SECRET", "baseUrl": "https://cloud.onyx.app/api", - "personaId": 7 + "path": "/chat/send-message-simple-api", + "useAnswerWithCitationModels": true, + "personaId": 7, + "searchSettings": { + "runSearch": "always", + "realTime": true + } } } } diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index 8fd0769a02..5c9199d29a 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -169,7 +169,7 @@ public class FreshdeskControllerTests [BitAutoData(WebhookKey)] public async Task PostWebhookOnyxAi_success( string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - OnyxAnswerWithCitationResponseModel onyxResponse, + OnyxResponseModel onyxResponse, SutProvider sutProvider) { var billingSettings = sutProvider.GetDependency>().Value; From 12ad758f72ba15ee852fa3f8e22df98db7a95716 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 9 Oct 2025 09:59:14 -0700 Subject: [PATCH 18/22] Add dedicated functions instead of updating existing --- ...nizationUsersControllerPerformanceTests.cs | 9 ++--- util/DbSeederUtility/Program.cs | 3 +- util/Seeder/Factories/OrganizationSeeder.cs | 40 ++++++++++--------- util/Seeder/Factories/UserSeeder.cs | 20 +++++++++- .../Recipes/OrganizationWithUsersRecipe.cs | 15 ++++--- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs index 7ed81baa48..d77a41f52e 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -1,9 +1,7 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; -using Bit.Core.Entities; using Bit.Seeder.Recipes; -using Microsoft.AspNetCore.Identity; using Xunit; using Xunit.Abstractions; @@ -11,7 +9,7 @@ namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) { - [Theory()] + [Theory(Skip = "Performance test")] [InlineData(100)] [InlineData(60000)] public async Task GetAsync(int seats) @@ -20,12 +18,11 @@ public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOu var client = factory.CreateClient(); var db = factory.GetDatabaseContext(); - var passwordHasher = factory.Services.CreateScope().ServiceProvider.GetService>(); - var seeder = new OrganizationWithUsersRecipe(db, passwordHasher); + var seeder = new OrganizationWithUsersRecipe(db); var orgId = seeder.Seed("Org", seats, "large.test"); - var tokens = await factory.LoginAsync("admin@large.test", "z0Cvc58Q/lLhEDgtGnuPIFS/INOZP2qFlzaqzvTNEJI="); + var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs index 2d9fc0a9ac..19c0e1bec6 100644 --- a/util/DbSeederUtility/Program.cs +++ b/util/DbSeederUtility/Program.cs @@ -37,9 +37,8 @@ public class Program using var scope = serviceProvider.CreateScope(); var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService(); - var passwordHasher = scopedServices.GetRequiredService>(); - var recipe = new OrganizationWithUsersRecipe(db, passwordHasher); + var recipe = new OrganizationWithUsersRecipe(db); recipe.Seed(name, users, domain); } } diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs index 6363829de5..f6f05d9525 100644 --- a/util/Seeder/Factories/OrganizationSeeder.cs +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -2,19 +2,14 @@ using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Models; -using Bit.RustSDK; namespace Bit.Seeder.Factories; public class OrganizationSeeder { - public static (Organization organization, string key) CreateEnterprise(string name, string domain, int seats) + public static Organization CreateEnterprise(string name, string domain, int seats) { - var nativeService = RustSdkServiceFactory.CreateSingleton(); - - var keys = nativeService.GenerateOrganizationKeys(); - - var organization = new Organization + return new Organization { Id = Guid.NewGuid(), Name = name, @@ -25,28 +20,37 @@ public class OrganizationSeeder // Currently hardcoded to the values from https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/client/test_accounts.rs. // TODO: These should be dynamically generated by the SDK. - PublicKey = keys.PublicKey, - PrivateKey = keys.PrivateKey, + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB", + PrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=", }; - - return (organization, keys.Key); } } -public static class OrganizationExtensions +public static class OrgnaizationExtensions { - public static OrganizationUser CreateOrganizationUser(this Organization organization, User user, string orgKey) + public static OrganizationUser CreateOrganizationUser(this Organization organization, User user) { - var nativeService = RustSdkServiceFactory.CreateSingleton(); - - var userOrgKey = nativeService.GenerateUserOrganizationKey(user.PublicKey!, orgKey); - return new OrganizationUser { Id = Guid.NewGuid(), OrganizationId = organization.Id, UserId = user.Id, - Key = userOrgKey, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Type = OrganizationUserType.Admin, + Status = OrganizationUserStatusType.Confirmed + }; + } + + public static OrganizationUser CreateSdkOrganizationUser(this Organization organization, User user) + { + return new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + UserId = user.Id, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", Type = OrganizationUserType.Admin, Status = OrganizationUserStatusType.Confirmed }; diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index d6f3ec1762..d4bcb5eba8 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -8,7 +8,25 @@ namespace Bit.Seeder.Factories; public class UserSeeder { - public static (User user, string userKey) CreateUser(IPasswordHasher passwordHasher, string email) + public static User CreateUser(string email) + { + return new User + { + Id = Guid.NewGuid(), + Email = email, + MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==", + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", + PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=", + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 600_000, + }; + } + + public static (User user, string userKey) CreateSdkUser(IPasswordHasher passwordHasher, string email) { var nativeService = RustSdkServiceFactory.CreateSingleton(); var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf"); diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs index c69bb893bb..fb06c091ae 100644 --- a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -1,26 +1,25 @@ -using Bit.Core.Entities; +using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Factories; using LinqToDB.EntityFrameworkCore; -using Microsoft.AspNetCore.Identity; namespace Bit.Seeder.Recipes; -public class OrganizationWithUsersRecipe(DatabaseContext db, IPasswordHasher passwordHasher) +public class OrganizationWithUsersRecipe(DatabaseContext db) { public Guid Seed(string name, int users, string domain) { - var (organization, orgKey) = OrganizationSeeder.CreateEnterprise(name, domain, users); - var (user, _) = UserSeeder.CreateUser(passwordHasher, $"admin@{domain}"); - var orgUser = organization.CreateOrganizationUser(user, orgKey); + var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); + var user = UserSeeder.CreateUser($"admin@{domain}"); + var orgUser = organization.CreateOrganizationUser(user); var additionalUsers = new List(); var additionalOrgUsers = new List(); for (var i = 0; i < users; i++) { - var (additionalUser, _) = UserSeeder.CreateUser(passwordHasher, $"user{i}@{domain}"); + var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); additionalUsers.Add(additionalUser); - additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser, orgKey)); + additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); } db.Add(organization); From 4192b08aa537cb5698c011b941a5bbce24fad430 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 9 Oct 2025 10:39:28 -0700 Subject: [PATCH 19/22] Configure csproj to build rust --- .github/workflows/test.yml | 8 ++++++++ util/RustSdk/RustSdk.csproj | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4eed6df7ab..718616c47b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,14 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + - name: Install rust + uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7 + - name: Print environment run: | dotnet --info diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj index a5b3c9385d..eeedcb4f9c 100644 --- a/util/RustSdk/RustSdk.csproj +++ b/util/RustSdk/RustSdk.csproj @@ -23,4 +23,8 @@ + + + + From 5784d74d5250e40c66ea771980fe06ec84a2ac26 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 9 Oct 2025 15:00:35 -0700 Subject: [PATCH 20/22] Add workaround for generated code --- util/RustSdk/RustSdk.csproj | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj index eeedcb4f9c..f47d4c5b66 100644 --- a/util/RustSdk/RustSdk.csproj +++ b/util/RustSdk/RustSdk.csproj @@ -21,10 +21,17 @@ true + + + - + + + + From 9a9c9918e2e899615c005834b0611ab401efdc27 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 9 Oct 2025 15:04:09 -0700 Subject: [PATCH 21/22] Undo changes to program.cs --- util/DbSeederUtility/Program.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs index 19c0e1bec6..8a2c688417 100644 --- a/util/DbSeederUtility/Program.cs +++ b/util/DbSeederUtility/Program.cs @@ -1,10 +1,7 @@ -using Bit.Core.Entities; -using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Seeder.Recipes; using CommandDotNet; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; namespace Bit.DbSeederUtility; @@ -29,7 +26,6 @@ public class Program // Create service provider with necessary services var services = new ServiceCollection(); ServiceCollectionExtension.ConfigureServices(services); - services.TryAddScoped, PasswordHasher>(); var serviceProvider = services.BuildServiceProvider(); From fdd7cd0e2fafcfe182214d8655331d55383802d0 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 9 Oct 2025 15:23:48 -0700 Subject: [PATCH 22/22] Change the paths to wildcard to avoid errors on other platforms --- util/RustSdk/RustSdk.csproj | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj index f47d4c5b66..ba16a55661 100644 --- a/util/RustSdk/RustSdk.csproj +++ b/util/RustSdk/RustSdk.csproj @@ -10,16 +10,20 @@ - + Always true runtimes/osx-arm64/native/libsdk.dylib - + + Always true + runtimes/linux-x64/native/libsdk.dylib - + + Always true + runtimes/windows-x64/native/libsdk.dylib