diff --git a/.github/workflows/build-rust-cross-platform.yml b/.github/workflows/build-rust-cross-platform.yml
new file mode 100644
index 00000000..845a13c3
--- /dev/null
+++ b/.github/workflows/build-rust-cross-platform.yml
@@ -0,0 +1,127 @@
+name: Build Rust Cross Platform
+
+on:
+ workflow_call:
+
+jobs:
+ build_rust:
+ name: Build for ${{ matrix.settings.os }} ${{ matrix.settings.target }}
+ runs-on: ${{ matrix.settings.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ settings:
+ - os: macos-13
+ target: x86_64-apple-darwin
+ dotnet_rid: osx-x64
+ - os: macos-13
+ target: aarch64-apple-darwin
+ dotnet_rid: osx-arm64
+ - os: windows-2022
+ target: i686-pc-windows-msvc
+ dotnet_rid: win-x86
+ - os: windows-2022
+ target: x86_64-pc-windows-msvc
+ dotnet_rid: win-x64
+ - os: windows-2022
+ target: aarch64-pc-windows-msvc
+ dotnet_rid: win-arm64
+ # caution: updating the linux runner OS version for GNU
+ # targets will likely break the library for older OS versions.
+ # prefer using oldest supported runner for for these targets
+ - os: ubuntu-22.04
+ target: x86_64-unknown-linux-gnu
+ dotnet_rid: linux-x64
+ - os: ubuntu-22.04
+ target: aarch64-unknown-linux-gnu
+ dotnet_rid: linux-arm64
+ use_cross: true
+ - os: ubuntu-22.04
+ target: armv7-unknown-linux-gnueabihf
+ dotnet_rid: linux-arm
+ use_cross: true
+ - os: ubuntu-22.04
+ target: arm-unknown-linux-gnueabihf
+ dotnet_rid: linux-armv6
+ use_cross: true
+ - os: ubuntu-22.04
+ target: armv5te-unknown-linux-gnueabi
+ dotnet_rid: linux-armel
+ use_cross: true
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - name: Install rust
+ uses: dtolnay/rust-toolchain@c5a29ddb4d9d194e7c84ec8c3fba61b1c31fee8c # stable
+ with:
+ toolchain: stable
+
+ - name: Cache cargo registry
+ uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
+
+ - name: Install cross
+ if: ${{ startsWith(matrix.settings.os, 'ubuntu') && !startsWith(matrix.settings.target, 'x86_64') }}
+ run: cargo install cross --git https://github.com/cross-rs/cross --rev 36c0d7810ddde073f603c82d896c2a6c886ff7a4
+
+ - name: Add build architecture
+ run: rustup target add ${{ matrix.settings.target }}
+
+ # Build Rust natively
+ - name: Build Rust native for - ${{ matrix.settings.target }}
+ if: ${{ matrix.settings.use_cross != true }}
+ env:
+ RUSTFLAGS: "-D warnings"
+ MACOSX_DEPLOYMENT_TARGET: "10.14" # allows using new macos runner versions while still supporting older systems
+ run: cargo build --target ${{ matrix.settings.target }} --release
+ working-directory: extensions/Bitwarden.Opaque/rust
+
+ # Build Rust using cross
+ - name: Build Rust cross for - ${{ matrix.settings.target }}
+ if: ${{ matrix.settings.use_cross == true }}
+ env:
+ RUSTFLAGS: "-D warnings"
+ run: cross build --target ${{ matrix.settings.target }} --release
+ working-directory: extensions/Bitwarden.Opaque/rust
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ with:
+ name: libopaque_ke_binding_files-${{ matrix.settings.dotnet_rid }}
+ # We only need these until the next step, so delete them as soon as possible
+ retention-days: 1
+ if-no-files-found: error
+ path: |
+ extensions/Bitwarden.Opaque/rust/target/${{ matrix.settings.target }}/release/opaque_ke_binding.dll
+ extensions/Bitwarden.Opaque/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.so
+ extensions/Bitwarden.Opaque/rust/target/${{ matrix.settings.target }}/release/libopaque_ke_binding.dylib
+
+ collect_artifacts:
+ name: Collect and Upload All Artifacts
+ runs-on: ubuntu-22.04
+ needs: build_rust
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ with:
+ pattern: libopaque_ke_binding_files-*
+ path: downloaded_runtimes/
+
+ - name: Move files to the correct directory
+ run: |
+ for file in downloaded_runtimes/libopaque_ke_binding_files-*; do
+ echo "Processing $file"
+ platform="${file#downloaded_runtimes/libopaque_ke_binding_files-}"
+ echo "Platform: $platform"
+ mkdir -p runtimes/${platform}/native
+ mv $file/* runtimes/${platform}/native
+ done
+
+ - name: Upload Combined Artifact
+ uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ with:
+ name: libopaque_ke_binding_all_files
+ if-no-files-found: error
+ path: |
+ runtimes/**/*
diff --git a/.github/workflows/pack-and-release.yml b/.github/workflows/pack-and-release.yml
index 649814b6..3ea66727 100644
--- a/.github/workflows/pack-and-release.yml
+++ b/.github/workflows/pack-and-release.yml
@@ -11,8 +11,14 @@ on:
description: Token used to publish packages
jobs:
+ build_rust:
+ name: Build Rust Cross Platform
+ uses: ./.github/workflows/build-rust-cross-platform.yml
+
release:
name: Release
+ needs:
+ - build_rust
runs-on: ubuntu-22.04
outputs:
package: ${{ steps.parse-package.outputs.result }}
@@ -47,6 +53,12 @@ jobs:
return refParts[3];
+ - name: Download cross compiled library files
+ uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ with:
+ name: libopaque_ke_binding_all_files
+ path: extensions/Bitwarden.Opaque/rust/dist/
+
- name: Pack
run: dotnet pack -c Release -p:IsPreRelease=$IS_PRERELEASE
working-directory: "./extensions/${{ steps.parse-package.outputs.result }}/src"
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index c20496f7..393a57ac 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -8,7 +8,7 @@ on:
jobs:
prerelease:
name: Do prerelease
- uses: bitwarden/dotnet-extensions/.github/workflows/pack-and-release.yml@main
+ uses: ./.github/workflows/pack-and-release.yml
with:
prerelease: true
secrets:
diff --git a/.github/workflows/start-release.yml b/.github/workflows/start-release.yml
index e9df2f28..4221847e 100644
--- a/.github/workflows/start-release.yml
+++ b/.github/workflows/start-release.yml
@@ -11,6 +11,7 @@ on:
options:
- Bitwarden.Server.Sdk
- Bitwarden.Server.Sdk.Features
+ - Bitwarden.Opaque
permissions:
pull-requests: write
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..b7ef8653
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "rust-analyzer.linkedProjects": ["extensions/Bitwarden.Opaque/rust/Cargo.toml"],
+}
diff --git a/bitwarden-dotnet.sln b/bitwarden-dotnet.sln
index 7e7e4498..3cd6ee85 100644
--- a/bitwarden-dotnet.sln
+++ b/bitwarden-dotnet.sln
@@ -45,6 +45,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4949B721
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Server.Sdk.Features.Tests", "extensions\Bitwarden.Server.Sdk.Features\tests\Bitwarden.Server.Sdk.Features.Tests\Bitwarden.Server.Sdk.Features.Tests.csproj", "{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bitwarden.Opaque", "Bitwarden.Opaque", "{023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque", "extensions\Bitwarden.Opaque\src\Bitwarden.Opaque.csproj", "{B49E33DF-A672-4361-BECA-C3DA423BD7A9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque.Tests", "extensions\Bitwarden.Opaque\tests\Bitwarden.Opaque.Tests.csproj", "{DC9DAB81-5ED7-4756-BF20-D594D87D2865}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Opaque.Benchmarks", "extensions\Bitwarden.Opaque\perf\Bitwarden.Opaque.Benchmarks.csproj", "{96631E35-695B-4BAF-B1E7-4446437A5FC8}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +110,18 @@ Global
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B49E33DF-A672-4361-BECA-C3DA423BD7A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DC9DAB81-5ED7-4756-BF20-D594D87D2865}.Release|Any CPU.Build.0 = Release|Any CPU
+ {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {96631E35-695B-4BAF-B1E7-4446437A5FC8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5EC8B943-2E9E-437D-9FFC-D18B5DB4D7D0} = {695C76EF-1102-4805-970F-7C995EE54930}
@@ -124,5 +144,9 @@ Global
{DF914CD1-F916-4A58-B749-625DB67FAAA7} = {026589E0-5AAA-44EB-B973-3CFFF5B54AFC}
{4949B721-5C7F-4D85-AB35-F57B54D7A6E6} = {026589E0-5AAA-44EB-B973-3CFFF5B54AFC}
{1789F567-87B3-4313-80CF-E3CCFA1B6D5E} = {4949B721-5C7F-4D85-AB35-F57B54D7A6E6}
+ {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1} = {695C76EF-1102-4805-970F-7C995EE54930}
+ {B49E33DF-A672-4361-BECA-C3DA423BD7A9} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}
+ {DC9DAB81-5ED7-4756-BF20-D594D87D2865} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}
+ {96631E35-695B-4BAF-B1E7-4446437A5FC8} = {023418F1-43B7-4D56-AFA1-67D8FE9B7EC1}
EndGlobalSection
EndGlobal
diff --git a/extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj b/extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj
new file mode 100644
index 00000000..4a6876aa
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net8.0
+ false
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extensions/Bitwarden.Opaque/perf/OpaqueBench.cs b/extensions/Bitwarden.Opaque/perf/OpaqueBench.cs
new file mode 100644
index 00000000..2162f83d
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/perf/OpaqueBench.cs
@@ -0,0 +1,108 @@
+using BenchmarkDotNet.Attributes;
+
+namespace Bitwarden.Opaque.Benchmarks;
+
+// dotnet run --project extensions/Bitwarden.Opaque/perf/Bitwarden.Opaque.Benchmarks.csproj -c Release -p:BuildOpaqueLib=true
+
+[MemoryDiagnoser]
+public class OpaqueBench
+{
+ public BitwardenOpaqueServer server = new();
+ public BitwardenOpaqueClient client = new();
+ public CipherConfiguration config = CipherConfiguration.Default;
+
+ public string username = "demo_username";
+ public string password = "demo_password";
+
+ public byte[] serverSetup = null!;
+ public byte[] serverRegistration = null!;
+
+ public byte[] clientRegistrationRequest = null!;
+ public byte[] clientRegistrationUpload = null!;
+
+ public byte[] clientLoginCredentialRequest = null!;
+ public byte[] serverLoginState = null!;
+ public byte[] clientLoginCredentialFinalization = null!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ // Use the complete benchmarks to extract the data for the partial benchmarks
+ var registration = CompleteRegistration();
+ serverSetup = registration.Item1;
+ serverRegistration = registration.Item2;
+ clientRegistrationRequest = registration.Item3;
+ clientRegistrationUpload = registration.Item4;
+
+ var login = CompleteLogin();
+ clientLoginCredentialRequest = login.Item1;
+ serverLoginState = login.Item2;
+ clientLoginCredentialFinalization = login.Item3;
+ }
+
+ [Benchmark]
+ public (byte[], byte[]) SeededFakeRegistration()
+ {
+ var seed = new byte[32];
+ return server.SeededFakeRegistration(seed);
+ }
+
+ [Benchmark]
+ public (byte[], byte[], byte[], byte[]) CompleteRegistration()
+ {
+ var clientRegisterStartResult = client.StartRegistration(config, password);
+ var serverRegisterStartResult = server.StartRegistration(config, null, clientRegisterStartResult.registrationRequest, username);
+ var clientRegisterFinishResult = client.FinishRegistration(config, clientRegisterStartResult.state, serverRegisterStartResult.registrationResponse, password);
+ var serverRegisterFinishResult = server.FinishRegistration(config, clientRegisterFinishResult.registrationUpload);
+ return (
+ serverRegisterStartResult.serverSetup,
+ serverRegisterFinishResult.serverRegistration,
+ clientRegisterStartResult.registrationRequest,
+ clientRegisterFinishResult.registrationUpload
+ );
+ }
+
+ [Benchmark]
+ public (byte[], byte[], byte[], byte[]) CompleteLogin()
+ {
+ var clientLoginStartResult = client.StartLogin(config, password);
+ var serverLoginStartResult = server.StartLogin(config, serverSetup, serverRegistration, clientLoginStartResult.credentialRequest, username);
+ var clientLoginFinishResult = client.FinishLogin(config, clientLoginStartResult.state, serverLoginStartResult.credentialResponse, password);
+ var serverLoginFinishResult = server.FinishLogin(config, serverLoginStartResult.state, clientLoginFinishResult.credentialFinalization);
+ return (
+ clientLoginStartResult.credentialRequest,
+ serverLoginStartResult.state,
+ clientLoginFinishResult.credentialFinalization,
+ serverLoginFinishResult.sessionKey
+ );
+ }
+
+ [Benchmark]
+ public (byte[], byte[]) StartServerRegistration()
+ {
+ var result = server.StartRegistration(config, null, clientRegistrationRequest, username);
+ return (result.registrationResponse, result.serverSetup);
+ }
+
+ [Benchmark]
+ public byte[] FinishServerRegistration()
+ {
+ var result = server.FinishRegistration(config, clientRegistrationUpload);
+ return result.serverRegistration;
+ }
+
+
+ [Benchmark]
+ public (byte[], byte[]) StartServerLogin()
+ {
+ var result = server.StartLogin(config, serverSetup, serverRegistration, clientLoginCredentialRequest, username);
+ return (result.credentialResponse, result.state);
+ }
+
+ [Benchmark]
+ public byte[] FinishServerLogin()
+ {
+ var result = server.FinishLogin(config, serverLoginState, clientLoginCredentialFinalization);
+ return result.sessionKey;
+ }
+}
diff --git a/extensions/Bitwarden.Opaque/perf/Program.cs b/extensions/Bitwarden.Opaque/perf/Program.cs
new file mode 100644
index 00000000..ae6c326d
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/perf/Program.cs
@@ -0,0 +1,4 @@
+using System.Reflection;
+using BenchmarkDotNet.Running;
+
+BenchmarkRunner.Run(Assembly.GetExecutingAssembly());
diff --git a/extensions/Bitwarden.Opaque/rust/.gitignore b/extensions/Bitwarden.Opaque/rust/.gitignore
new file mode 100644
index 00000000..580f45e9
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/rust/.gitignore
@@ -0,0 +1,2 @@
+target
+dist
diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.lock b/extensions/Bitwarden.Opaque/rust/Cargo.lock
new file mode 100644
index 00000000..8bc6d84a
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/rust/Cargo.lock
@@ -0,0 +1,643 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures",
+ "password-hash",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64ct"
+version = "1.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "fiat-crypto",
+ "rand_core",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "derive-where"
+version = "1.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "serde",
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "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 = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.171"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+
+[[package]]
+name = "log"
+version = "0.4.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "once_cell"
+version = "1.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
+
+[[package]]
+name = "opaque-ke"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb31f7b2c5760d8ffbc39652043f5cff14029b6249a2280b28ae6f0cd61f5098"
+dependencies = [
+ "argon2",
+ "curve25519-dalek",
+ "derive-where",
+ "digest",
+ "displaydoc",
+ "elliptic-curve",
+ "generic-array",
+ "getrandom",
+ "hkdf",
+ "hmac",
+ "rand",
+ "serde",
+ "subtle",
+ "voprf",
+ "zeroize",
+]
+
+[[package]]
+name = "opaque-ke-binding"
+version = "0.0.0"
+dependencies = [
+ "argon2",
+ "hex",
+ "opaque-ke",
+ "rand",
+ "rand_chacha",
+ "serde",
+ "serde_json",
+ "zeroizing-alloc",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "voprf"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28f59c30c76e2fea54cdece6a054e2662feffa7ab19658a7887524265ee39470"
+dependencies = [
+ "curve25519-dalek",
+ "derive-where",
+ "digest",
+ "displaydoc",
+ "elliptic-curve",
+ "generic-array",
+ "rand_core",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zeroizing-alloc"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebff5e6b81c1c7dca2d0bd333b2006da48cb37dbcae5a8da888f31fcb3c19934"
diff --git a/extensions/Bitwarden.Opaque/rust/Cargo.toml b/extensions/Bitwarden.Opaque/rust/Cargo.toml
new file mode 100644
index 00000000..1e6a144c
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/rust/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "opaque-ke-binding"
+version = "0.0.0"
+edition = "2024"
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+argon2 = "0.5.3"
+hex = "0.4.3"
+opaque-ke = { version = "3.0.0", features = ["std", "argon2"] }
+rand = "0.8.5"
+rand_chacha = "0.3.1"
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
+zeroizing-alloc = "0.1.0"
+
+# We want panic unwinding to be enabled, which would ensure the process it's not aborted
+# when a panic occurs. This the default value, but we want to be explicit about it.
+
+[profile.dev]
+panic = "unwind"
+
+[profile.release]
+panic = "unwind"
+# Turn on LTO on release mode
+lto = true
+codegen-units = 1
diff --git a/extensions/Bitwarden.Opaque/rust/rust-toolchain.toml b/extensions/Bitwarden.Opaque/rust/rust-toolchain.toml
new file mode 100644
index 00000000..73cb934d
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/rust/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "stable"
+components = ["rustfmt", "clippy"]
diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs
new file mode 100644
index 00000000..90b9d403
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/rust/src/ffi/mod.rs
@@ -0,0 +1,317 @@
+use std::{ffi::c_char, panic::UnwindSafe, str::FromStr};
+
+use crate::{
+ Error,
+ opaque::{CipherConfiguration, OpaqueImpl},
+ try_ffi,
+};
+
+mod types;
+
+use types::*;
+
+/// # Safety
+/// All the limitations of [`std::ffi::CStr::from_ptr`] apply, mainly:
+/// - The pointer must be valid and point to a null-terminated byte string.
+/// - The memory must be valid for the duration of the call and not modified by other threads.
+unsafe fn parse_str<'a>(input: *const c_char, name: &'static str) -> Result<&'a str, Error> {
+ if input.is_null() {
+ return Err(Error::InvalidInput("Input string is null".into()));
+ }
+ unsafe { std::ffi::CStr::from_ptr(input) }
+ .to_str()
+ .map_err(|_| Error::InvalidInput(name.into()))
+}
+
+fn catch(f: impl FnOnce() -> Response + UnwindSafe) -> Response {
+ match std::panic::catch_unwind(f) {
+ Ok(r) => r,
+ Err(e) => Response::error(Error::InternalError(format!("Panic: {e:?}"))),
+ }
+}
+
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn register_seeded_fake_config(seed: Buffer) -> Response {
+ catch(|| {
+ let seed = try_ffi!(unsafe { seed.as_slice() });
+ let seed = try_ffi!(
+ <[u8; 32]>::try_from(seed)
+ .map_err(|_| Error::InvalidInput("Seed must be 32 bytes".into()))
+ );
+
+ let (server_setup, server_registration) =
+ try_ffi!(crate::opaque::register_seeded_fake_config(seed));
+ Response::ok([server_setup, server_registration])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn start_client_registration(
+ config: *const c_char,
+ password: *const c_char,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let password: &str = try_ffi!(unsafe { parse_str(password, "password") });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let result = try_ffi!(config.start_client_registration(password));
+ Response::ok([result.registration_request, result.state])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn start_server_registration(
+ config: *const c_char,
+ server_setup: Buffer,
+ registration_request: Buffer,
+ username: *const c_char,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let server_setup = try_ffi!(unsafe { server_setup.as_slice_optional() });
+ let registration_request = try_ffi!(unsafe { registration_request.as_slice() });
+ let username = try_ffi!(unsafe { parse_str(username, "username") });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response = try_ffi!(config.start_server_registration(
+ server_setup,
+ registration_request,
+ username
+ ));
+ Response::ok([response.registration_response, response.server_setup])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn finish_client_registration(
+ config: *const c_char,
+ state: Buffer,
+ registration_response: Buffer,
+ password: *const c_char,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let registration_response = try_ffi!(unsafe { registration_response.as_slice() });
+ let state = try_ffi!(unsafe { state.as_slice() });
+ let password = try_ffi!(unsafe { parse_str(password, "password") });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response =
+ try_ffi!(config.finish_client_registration(state, registration_response, password));
+ Response::ok([
+ response.registration_upload,
+ response.export_key,
+ response.server_s_pk,
+ ])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn finish_server_registration(
+ config: *const c_char,
+ registration_upload: Buffer,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let registration_upload = try_ffi!(unsafe { registration_upload.as_slice() });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response = try_ffi!(config.finish_server_registration(registration_upload));
+ Response::ok([response.server_registration])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn start_client_login(
+ config: *const c_char,
+ password: *const c_char,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let password = try_ffi!(unsafe { parse_str(password, "password") });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response = try_ffi!(config.start_client_login(password));
+ Response::ok([response.credential_request, response.state])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn start_server_login(
+ config: *const c_char,
+ server_setup: Buffer,
+ server_registration: Buffer,
+ credential_request: Buffer,
+ username: *const c_char,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let server_setup = try_ffi!(unsafe { server_setup.as_slice() });
+ let server_registration = try_ffi!(unsafe { server_registration.as_slice() });
+ let credential_request = try_ffi!(unsafe { credential_request.as_slice() });
+ let username = try_ffi!(unsafe { parse_str(username, "username") });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response = try_ffi!(config.start_server_login(
+ server_setup,
+ server_registration,
+ credential_request,
+ username,
+ ));
+ Response::ok([response.credential_response, response.state])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn finish_client_login(
+ config: *const c_char,
+ state: Buffer,
+ credential_response: Buffer,
+ password: *const c_char,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let state = try_ffi!(unsafe { state.as_slice() });
+ let credential_response = try_ffi!(unsafe { credential_response.as_slice() });
+ let password = try_ffi!(unsafe { parse_str(password, "password") });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response = try_ffi!(config.finish_client_login(state, credential_response, password));
+ Response::ok([
+ response.credential_finalization,
+ response.session_key,
+ response.export_key,
+ response.server_s_pk,
+ ])
+ })
+}
+
+/// # Safety
+/// This function must follow the same safety rules as [`parse_str`] and [`Buffer::as_slice`].
+/// The caller must ensure that the [Response] is correctly freed after use.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn finish_server_login(
+ config: *const c_char,
+ state: Buffer,
+ credential_finalization: Buffer,
+) -> Response {
+ catch(|| {
+ let config: &str = try_ffi!(unsafe { parse_str(config, "config") });
+ let state = try_ffi!(unsafe { state.as_slice() });
+ let credential_finalization = try_ffi!(unsafe { credential_finalization.as_slice() });
+
+ let mut config = try_ffi!(CipherConfiguration::from_str(config));
+
+ let response = try_ffi!(config.finish_server_login(state, credential_finalization));
+ Response::ok([response.session_key])
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ffi::{CString, c_char};
+
+ use crate::opaque::*;
+
+ use super::{types::Buffer, *};
+
+ // Test for possible panics and/or undefined behavior.
+ // Ideally run using:
+ //
+ // cargo +nightly miri test
+ // RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target aarch64-apple-darwin --release
+ // RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test --target aarch64-apple-darwin --release
+ #[test]
+ fn test_ffi_no_panic() {
+ let user = "username";
+ let user = CString::new(user).unwrap();
+ let pass = "password";
+ let pass = CString::new(pass).unwrap();
+ let cfg = serde_json::to_string(&CipherConfiguration::default()).unwrap();
+ let cfg = CString::new(cfg.as_str()).unwrap();
+
+ let _buf = Buffer::from_vec(vec![0; 32]);
+ let buf = || _buf.duplicate();
+
+ let _null_buf = Buffer::null();
+ let null_buf = || _null_buf.duplicate();
+
+ let null_str_ptr: *const c_char = std::ptr::null();
+
+ unsafe {
+ start_client_registration(cfg.as_ptr(), pass.as_ptr());
+ start_client_registration(null_str_ptr, pass.as_ptr());
+ start_client_registration(null_str_ptr, null_str_ptr);
+
+ start_server_registration(cfg.as_ptr(), buf(), buf(), user.as_ptr());
+ start_server_registration(null_str_ptr, buf(), buf(), user.as_ptr());
+ start_server_registration(cfg.as_ptr(), buf(), buf(), user.as_ptr());
+ start_server_registration(cfg.as_ptr(), null_buf(), buf(), user.as_ptr());
+ start_server_registration(cfg.as_ptr(), buf(), null_buf(), null_str_ptr);
+
+ finish_client_registration(cfg.as_ptr(), buf(), buf(), pass.as_ptr());
+ finish_client_registration(null_str_ptr, buf(), buf(), pass.as_ptr());
+ finish_client_registration(cfg.as_ptr(), null_buf(), buf(), pass.as_ptr());
+ finish_client_registration(cfg.as_ptr(), buf(), null_buf(), pass.as_ptr());
+ finish_client_registration(cfg.as_ptr(), buf(), buf(), null_str_ptr);
+
+ finish_server_registration(cfg.as_ptr(), buf());
+ finish_server_registration(null_str_ptr, buf());
+ finish_server_registration(cfg.as_ptr(), null_buf());
+
+ start_client_login(cfg.as_ptr(), pass.as_ptr());
+ start_client_login(null_str_ptr, pass.as_ptr());
+ start_client_login(cfg.as_ptr(), null_str_ptr);
+
+ start_server_login(cfg.as_ptr(), buf(), buf(), buf(), user.as_ptr());
+ start_server_login(null_str_ptr, buf(), buf(), buf(), user.as_ptr());
+ start_server_login(cfg.as_ptr(), null_buf(), buf(), buf(), user.as_ptr());
+ start_server_login(cfg.as_ptr(), buf(), null_buf(), buf(), user.as_ptr());
+ start_server_login(cfg.as_ptr(), buf(), buf(), null_buf(), user.as_ptr());
+ start_server_login(cfg.as_ptr(), buf(), buf(), buf(), null_str_ptr);
+
+ finish_client_login(cfg.as_ptr(), buf(), buf(), pass.as_ptr());
+ finish_client_login(null_str_ptr, buf(), buf(), pass.as_ptr());
+ finish_client_login(cfg.as_ptr(), null_buf(), buf(), pass.as_ptr());
+ finish_client_login(cfg.as_ptr(), buf(), null_buf(), pass.as_ptr());
+ finish_client_login(cfg.as_ptr(), buf(), buf(), null_str_ptr);
+
+ finish_server_login(cfg.as_ptr(), buf(), buf());
+ finish_server_login(null_str_ptr, buf(), buf());
+ finish_server_login(cfg.as_ptr(), null_buf(), buf());
+ finish_server_login(cfg.as_ptr(), buf(), null_buf());
+
+ buf().free();
+ null_buf().free();
+ }
+ }
+}
diff --git a/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs
new file mode 100644
index 00000000..38238338
--- /dev/null
+++ b/extensions/Bitwarden.Opaque/rust/src/ffi/types.rs
@@ -0,0 +1,160 @@
+use crate::Error;
+
+/// Free the buffer memory.
+///
+/// # Safety
+/// This function should ONLY be used with [Buffer]s initialized from the Rust side.
+/// Calling it with a [Buffer] initialized from the C# side is undefined behavior.
+/// The caller is responsible for ensuring that the memory is not used after this call.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn free_buffer(buf: Buffer) {
+ std::panic::catch_unwind(|| {
+ unsafe { buf.free() };
+ })
+ .ok();
+}
+
+#[repr(C)]
+pub struct Buffer {
+ data: *mut u8,
+ len: usize,
+}
+
+/// A struct to represent a buffer of data.
+/// Important: The structure of this type must match the structure
+/// of the Buffer type in the C# `BitwardenLibrary`, both in field type and order.
+impl Buffer {
+ pub fn null() -> Self {
+ Buffer {
+ data: std::ptr::null_mut(),
+ len: 0,
+ }
+ }
+
+ pub fn from_vec(mut vec: Vec) -> Self {
+ // Important: Ensure that capacity and length are the same.
+ vec.shrink_to_fit();
+
+ let len = vec.len();
+ let data = vec.as_mut_ptr();
+ std::mem::forget(vec);
+ Buffer { data, len }
+ }
+
+ /// # Safety
+ /// All the limitations of [`std::slice::from_raw_parts`] apply, mainly:
+ /// - The pointer must be either null or valid for reads up to [`Buffer::len`] bytes.
+ /// - The memory must be valid for the duration of the call and not modified by other threads.
+ pub unsafe fn as_slice_optional(&self) -> Result