1
0
mirror of https://github.com/bitwarden/server synced 2025-12-19 09:43:25 +00:00

Wire up crypto logic for sharing org key

This commit is contained in:
Hinton
2025-07-31 14:48:48 +02:00
parent 75f11f68ac
commit 3132e09e21
9 changed files with 133 additions and 95 deletions

View File

@@ -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<IPasswordHasher<User>>();
var seeder = new OrganizationWithUsersRecipe(db, passwordHasher);
var orgId = seeder.Seed("Org", seats, "large.test");

View File

@@ -7,11 +7,26 @@ namespace Bit.RustSDK;
public class UserKeys
{
public required string MasterPasswordHash { get; set; }
/// <summary>
/// Base64 encoded UserKey
/// </summary>
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
{
/// <summary>
/// Base64 encoded SymmetricCryptoKey
/// </summary>
public required string Key { get; set; }
public required string PublicKey { get; set; }
public required string PrivateKey { get; set; }
}
/// <summary>
/// Service implementation that provides a C# friendly interface to the Rust SDK
/// </summary>
@@ -38,42 +53,31 @@ public class RustSdkService
}
}
/// <summary>
/// Hashes a password using the native implementation
/// </summary>
/// <param name="email">User email</param>
/// <param name="password">User password</param>
/// <returns>The hashed password as a string</returns>
/// <exception cref="ArgumentNullException">Thrown when email or password is null</exception>
/// <exception cref="ArgumentException">Thrown when email or password is empty</exception>
/// <exception cref="RustSdkException">Thrown when the native operation fails</exception>
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
var result = TakeAndDestroyRustString(resultPtr);
return JsonSerializer.Deserialize<OrganizationKeys>(result, CaseInsensitiveOptions)!;
}
public unsafe string GenerateUserOrganizationKey(string userKey, string orgKey)
{
fixed (byte* emailPtr = emailBytes)
fixed (byte* passwordPtr = passwordBytes)
var userKeyBytes = StringToRustString(userKey);
var orgKeyBytes = StringToRustString(orgKey);
fixed (byte* userKeyPtr = userKeyBytes)
fixed (byte* orgKeyPtr = orgKeyBytes)
{
var resultPtr = NativeMethods.hash_password(emailPtr, passwordPtr);
var resultPtr = NativeMethods.generate_user_organization_key(userKeyPtr, orgKeyPtr);
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 byte[] StringToRustString(string str)
{

View File

@@ -5,15 +5,6 @@
/// </summary>
public static class RustSdkServiceFactory
{
/// <summary>
/// Creates a new instance of the Rust SDK service
/// </summary>
/// <returns>A new IRustSdkService instance</returns>
public static RustSdkService Create()
{
return new RustSdkService();
}
/// <summary>
/// Creates a singleton instance of the Rust SDK service (thread-safe)
/// </summary>
@@ -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();
}
}

View File

@@ -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",

View File

@@ -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"

View File

@@ -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)
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

View File

@@ -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
};

View File

@@ -9,7 +9,7 @@ namespace Bit.Seeder.Factories;
public class UserSeeder
{
public static User CreateUser(IPasswordHasher<User> passwordHasher, string email)
public static (User user, string userKey) CreateUser(IPasswordHasher<User> 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);
}
}

View File

@@ -11,28 +11,28 @@ public class OrganizationWithUsersRecipe(DatabaseContext db, IPasswordHasher<Use
{
public Guid Seed(string name, int users, string domain)
{
var organization = OrganizationSeeder.CreateEnterprise(name, domain, users);
var user = UserSeeder.CreateUser(passwordHasher, $"admin@{domain}");
var orgUser = organization.CreateOrganizationUser(user);
var (organization, orgKey) = OrganizationSeeder.CreateEnterprise(name, domain, users);
var (user, _) = UserSeeder.CreateUser(passwordHasher, $"admin@{domain}");
var orgUser = organization.CreateOrganizationUser(user, orgKey);
var additionalUsers = new List<User>();
var additionalOrgUsers = new List<OrganizationUser>();
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;
}