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