1
0
mirror of https://github.com/bitwarden/server synced 2025-12-13 06:43:45 +00:00

Generate valid keys using rust

This commit is contained in:
Hinton
2025-07-31 10:20:53 +02:00
parent 072f9f2278
commit 75f11f68ac
7 changed files with 108 additions and 40 deletions

View File

@@ -1,7 +1,10 @@
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Seeder.Recipes; using Bit.Seeder.Recipes;
using CommandDotNet; using CommandDotNet;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Bit.DbSeederUtility; namespace Bit.DbSeederUtility;
@@ -26,14 +29,17 @@ public class Program
// Create service provider with necessary services // Create service provider with necessary services
var services = new ServiceCollection(); var services = new ServiceCollection();
ServiceCollectionExtension.ConfigureServices(services); ServiceCollectionExtension.ConfigureServices(services);
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
// Get a scoped DB context // Get a scoped DB context
using var scope = serviceProvider.CreateScope(); using var scope = serviceProvider.CreateScope();
var scopedServices = scope.ServiceProvider; var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<DatabaseContext>(); var db = scopedServices.GetRequiredService<DatabaseContext>();
var passwordHasher = scopedServices.GetRequiredService<IPasswordHasher<User>>();
var recipe = new OrganizationWithUsersRecipe(db); var recipe = new OrganizationWithUsersRecipe(db, passwordHasher);
recipe.Seed(name, users, domain); recipe.Seed(name, users, domain);
} }
} }

View File

@@ -1,28 +1,40 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.Json;
namespace Bit.RustSDK; 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; }
}
/// <summary> /// <summary>
/// Service implementation that provides a C# friendly interface to the Rust SDK /// Service implementation that provides a C# friendly interface to the Rust SDK
/// </summary> /// </summary>
public class RustSdkService public class RustSdkService
{ {
/// <summary> private static readonly JsonSerializerOptions CaseInsensitiveOptions = new()
/// Adds two integers using the native implementation
/// </summary>
/// <param name="x">First integer</param>
/// <param name="y">Second integer</param>
/// <returns>The sum of x and y</returns>
public int Add(int x, int y)
{ {
try PropertyNameCaseInsensitive = true
};
public unsafe UserKeys GenerateUserKeys(string email, string password)
{ {
return NativeMethods.my_add(x, y); var emailBytes = StringToRustString(email);
} var passwordBytes = StringToRustString(password);
catch (Exception ex)
fixed (byte* emailPtr = emailBytes)
fixed (byte* passwordPtr = passwordBytes)
{ {
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<UserKeys>(result, CaseInsensitiveOptions)!;
} }
} }
@@ -38,8 +50,8 @@ public class RustSdkService
public unsafe string HashPassword(string email, string password) public unsafe string HashPassword(string email, string password)
{ {
// Convert strings to null-terminated byte arrays // Convert strings to null-terminated byte arrays
var emailBytes = Encoding.UTF8.GetBytes(email + '\0'); var emailBytes = StringToRustString(email);
var passwordBytes = Encoding.UTF8.GetBytes(password + '\0'); var passwordBytes = StringToRustString(password);
try 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) private static unsafe string TakeAndDestroyRustString(byte* ptr)
{ {
if (ptr == null) if (ptr == null)

View File

@@ -184,7 +184,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_qs", "serde_qs",
"serde_repr", "serde_repr",
"thiserror 1.0.69", "thiserror 2.0.12",
"uuid", "uuid",
"zeroize", "zeroize",
] ]
@@ -220,7 +220,7 @@ dependencies = [
"sha1", "sha1",
"sha2", "sha2",
"subtle", "subtle",
"thiserror 1.0.69", "thiserror 2.0.12",
"typenum", "typenum",
"uuid", "uuid",
"zeroize", "zeroize",
@@ -252,7 +252,7 @@ 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=b0c950dad701bc419c76e8a7d37bf5c17a6909d6#b0c950dad701bc419c76e8a7d37bf5c17a6909d6"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"thiserror 1.0.69", "thiserror 2.0.12",
] ]
[[package]] [[package]]
@@ -1802,7 +1802,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1897,6 +1897,8 @@ dependencies = [
"bitwarden-core", "bitwarden-core",
"bitwarden-crypto", "bitwarden-crypto",
"csbindgen", "csbindgen",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@@ -1970,9 +1972,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",

View File

@@ -14,6 +14,8 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" } bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "b0c950dad701bc419c76e8a7d37bf5c17a6909d6" }
bitwarden-crypto = { 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] [build-dependencies]
csbindgen = "=1.9.3" csbindgen = "=1.9.3"

View File

@@ -1,3 +1,4 @@
#![allow(clippy::missing_safety_doc)]
use std::{ use std::{
ffi::{c_char, CStr, CString}, ffi::{c_char, CStr, CString},
num::NonZeroU32, num::NonZeroU32,
@@ -10,6 +11,39 @@ pub extern "C" fn my_add(x: i32, y: i32) -> i32 {
x + y 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 /// # Safety
/// ///
/// The `email` and `password` pointers must be valid null-terminated C strings. /// The `email` and `password` pointers must be valid null-terminated C strings.

View File

@@ -1,31 +1,36 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.RustSDK; using Bit.RustSDK;
using Microsoft.AspNetCore.Identity;
namespace Bit.Seeder.Factories; namespace Bit.Seeder.Factories;
public class UserSeeder public class UserSeeder
{ {
public static User CreateUser(string email)
public static User CreateUser(IPasswordHasher<User> passwordHasher, string email)
{ {
var nativeService = RustSdkServiceFactory.CreateSingleton(); var nativeService = RustSdkServiceFactory.CreateSingleton();
Console.WriteLine(NativeMethods.my_add(2, 3)); var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf");
var password = nativeService.HashPassword(email, "asdfasdfasdf"); var user = new User
return new User
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
Email = email, Email = email,
MasterPassword = password, MasterPassword = null,
SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609",
Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", Key = keys.EncryptedUserKey,
PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", PublicKey = keys.PublicKey,
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=", PrivateKey = keys.PrivateKey,
ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR",
Kdf = KdfType.PBKDF2_SHA256, Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 600_000, KdfIterations = 600_000,
}; };
user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash);
return user;
} }
} }

View File

@@ -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.Infrastructure.EntityFramework.Repositories;
using Bit.Seeder.Factories; using Bit.Seeder.Factories;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
namespace Bit.Seeder.Recipes; namespace Bit.Seeder.Recipes;
public class OrganizationWithUsersRecipe(DatabaseContext db) public class OrganizationWithUsersRecipe(DatabaseContext db, IPasswordHasher<User> passwordHasher)
{ {
public Guid Seed(string name, int users, string domain) public Guid Seed(string name, int users, string domain)
{ {
var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); 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 orgUser = organization.CreateOrganizationUser(user);
var additionalUsers = new List<User>(); var additionalUsers = new List<User>();
var additionalOrgUsers = new List<OrganizationUser>(); var additionalOrgUsers = new List<OrganizationUser>();
for (var i = 0; i < users; i++) 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); additionalUsers.Add(additionalUser);
additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser));
} }
db.Add(organization); //db.Add(organization);
db.Add(user); db.Add(user);
db.Add(orgUser); //db.Add(orgUser);
db.SaveChanges(); db.SaveChanges();
// Use LinqToDB's BulkCopy for significant better performance // Use LinqToDB's BulkCopy for significant better performance
db.BulkCopy(additionalUsers); //db.BulkCopy(additionalUsers);
db.BulkCopy(additionalOrgUsers); //db.BulkCopy(additionalOrgUsers);
return organization.Id; return organization.Id;
} }