1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-23 12:31:06 -05:00

Simple data seed generator (users and ciphers), loader, and extracter that can load from json or directly from seed generation - cannot be used to login as encryption methods are non functional

This commit is contained in:
Robert Y 2025-03-13 16:28:22 -06:00
parent 2df4076a6b
commit 4737d9d95c
20 changed files with 1774 additions and 0 deletions

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Bit.DbSeederUtility</RootNamespace>
<AssemblyName>DbSeeder</AssemblyName>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Seeder\Seeder.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandDotNet" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<None Include="..\Seeder\appsettings.json" Link="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,83 @@
using CommandDotNet;
using Bit.Seeder.Commands;
using Bit.Seeder.Settings;
namespace Bit.DbSeederUtility;
public class Program
{
private static int Main(string[] args)
{
// Ensure global settings are loaded
var globalSettings = GlobalSettingsFactory.GlobalSettings;
// Set the current directory to the seeder directory for consistent seed paths
var seederDirectory = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "seeder"));
Directory.SetCurrentDirectory(seederDirectory);
return new AppRunner<Program>()
.Run(args);
}
[Command("generate", Description = "Generate seed data as JSON files")]
public int Generate(
[Option('u', "users", Description = "Number of users to generate")]
int users,
[Option('c', "ciphers-per-user", Description = "Number of ciphers per user to generate")]
int ciphersPerUser,
[Option('n', "seed-name", Description = "Name for the seed data files")]
string seedName
)
{
// Execute the generate command
var generateCommand = new GenerateCommand();
return generateCommand.Execute(users, ciphersPerUser, seedName, false) ? 0 : 1;
}
[Command("load", Description = "Load seed data from JSON files into the database")]
public int Load(
[Option('n', "seed-name", Description = "Name of the seed data to load")]
string seedName,
[Option('t', "timestamp", Description = "Specific timestamp of the seed data to load (defaults to most recent)")]
string? timestamp = null,
[Option('d', "dry-run", Description = "Validate the seed data without actually loading it")]
bool dryRun = false
)
{
// Execute the load command
var loadCommand = new LoadCommand();
return loadCommand.Execute(seedName, timestamp, dryRun) ? 0 : 1;
}
[Command("generate-direct-load", Description = "Generate seed data and load it directly into the database without creating JSON files")]
public int GenerateDirectLoad(
[Option('u', "users", Description = "Number of users to generate")]
int users,
[Option('c', "ciphers-per-user", Description = "Number of ciphers per user to generate")]
int ciphersPerUser,
[Option('n', "seed-name", Description = "Name identifier for this seed operation")]
string seedName
)
{
// Execute the generate command with loadImmediately=true
var generateCommand = new GenerateCommand();
return generateCommand.Execute(users, ciphersPerUser, seedName, true) ? 0 : 1;
}
[Command("extract", Description = "Extract data from the database into seed files")]
public int Extract(
[Option('n', "seed-name", Description = "Name for the extracted seed")]
string seedName
)
{
// Execute the extract command
var extractCommand = new ExtractCommand();
return extractCommand.Execute(seedName) ? 0 : 1;
}
}

View File

@ -0,0 +1,119 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Bit.Seeder.Services;
using Bit.Seeder.Settings;
using Microsoft.AspNetCore.DataProtection;
using Bit.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Bit.Core.Enums;
namespace Bit.Seeder.Commands
{
public class ExtractCommand
{
public bool Execute(string seedName)
{
try
{
// Create service provider with necessary services
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
// Get the necessary services
var seederService = serviceProvider.GetRequiredService<ISeederService>();
var logger = serviceProvider.GetRequiredService<ILogger<ExtractCommand>>();
logger.LogInformation($"Extracting data from database to seed: {seedName}");
// Execute the extract operation
seederService.ExtractSeedsAsync(seedName).GetAwaiter().GetResult();
logger.LogInformation("Seed extraction completed successfully");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error extracting seeds: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
Console.WriteLine($"Stack trace: {ex.StackTrace}");
return false;
}
}
private void ConfigureServices(ServiceCollection services)
{
// Add logging
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
// Add JSON file configuration for other app settings not in GlobalSettings
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddUserSecrets("Bit.Seeder")
.Build();
services.AddSingleton<IConfiguration>(configuration);
// Get global settings
var globalSettings = GlobalSettingsFactory.GlobalSettings;
services.AddSingleton(globalSettings);
// Configure database provider
var provider = globalSettings.DatabaseProvider.ToLowerInvariant() switch
{
"postgres" or "postgresql" => SupportedDatabaseProviders.Postgres,
"mysql" or "mariadb" => SupportedDatabaseProviders.MySql,
"sqlite" => SupportedDatabaseProviders.Sqlite,
_ => SupportedDatabaseProviders.SqlServer
};
var connectionString = provider switch
{
SupportedDatabaseProviders.Postgres => globalSettings.PostgreSql?.ConnectionString,
SupportedDatabaseProviders.MySql => globalSettings.MySql?.ConnectionString,
SupportedDatabaseProviders.Sqlite => globalSettings.Sqlite?.ConnectionString,
_ => globalSettings.SqlServer?.ConnectionString
};
// Register database context
services.AddDbContext<DatabaseContext>(options =>
{
switch (provider)
{
case SupportedDatabaseProviders.Postgres:
options.UseNpgsql(connectionString);
break;
case SupportedDatabaseProviders.MySql:
options.UseMySql(connectionString!, ServerVersion.AutoDetect(connectionString!));
break;
case SupportedDatabaseProviders.Sqlite:
options.UseSqlite(connectionString!);
break;
default:
options.UseSqlServer(connectionString!);
break;
}
});
// Add Data Protection services
services.AddDataProtection()
.SetApplicationName("Bitwarden");
// Register other services
services.AddTransient<ISeederService, SeederService>();
services.AddTransient<IDatabaseService, DatabaseService>();
services.AddTransient<IEncryptionService, EncryptionService>();
}
}
}

View File

@ -0,0 +1,77 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Bit.Seeder.Services;
using Bit.Seeder.Settings;
using Microsoft.AspNetCore.DataProtection;
using Bit.Core;
namespace Bit.Seeder.Commands
{
public class GenerateCommand
{
public bool Execute(int users, int ciphersPerUser, string seedName, bool loadImmediately = false)
{
try
{
// Create service provider with necessary services
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
// Get the seeder service
var seederService = serviceProvider.GetRequiredService<ISeederService>();
var logger = serviceProvider.GetRequiredService<ILogger<GenerateCommand>>();
logger.LogInformation($"Generating seeds for {users} users with {ciphersPerUser} ciphers per user");
logger.LogInformation($"Seed name: {seedName}");
// Execute the appropriate action based on whether we need to load immediately
if (loadImmediately)
{
// Generate and load directly without creating intermediate files
seederService.GenerateAndLoadSeedsAsync(users, ciphersPerUser, seedName).GetAwaiter().GetResult();
logger.LogInformation("Seeds generated and loaded directly to database");
}
else
{
// Only generate seeds and save to files
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
seederService.GenerateSeedsAsync(users, ciphersPerUser, seedName).GetAwaiter().GetResult();
logger.LogInformation("Seed generation completed successfully");
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error generating seeds: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
Console.WriteLine($"Stack trace: {ex.StackTrace}");
return false;
}
}
private void ConfigureServices(ServiceCollection services)
{
// Load configuration using the GlobalSettingsFactory
var globalSettings = GlobalSettingsFactory.GlobalSettings;
// Register services
services.AddLogging(builder => builder.AddConsole());
services.AddSingleton(globalSettings);
// Add Data Protection services
services.AddDataProtection()
.SetApplicationName("Bitwarden");
// Register DatabaseContext and services
services.AddTransient<ISeederService, SeederService>();
services.AddTransient<IDatabaseService, DatabaseService>();
services.AddTransient<IEncryptionService, EncryptionService>();
services.AddDbContext<DatabaseContext>();
}
}
}

View File

@ -0,0 +1,87 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Bit.Seeder.Services;
using Bit.Seeder.Settings;
using Microsoft.AspNetCore.DataProtection;
using Bit.Core;
namespace Bit.Seeder.Commands
{
public class LoadCommand
{
public bool Execute(string seedName, string? timestamp = null, bool dryRun = false)
{
try
{
// Create service provider with necessary services
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
// Get the necessary services
var seederService = serviceProvider.GetRequiredService<ISeederService>();
var databaseService = serviceProvider.GetRequiredService<IDatabaseService>();
var logger = serviceProvider.GetRequiredService<ILogger<LoadCommand>>();
logger.LogInformation($"Loading seeds named: {seedName}");
if (!string.IsNullOrEmpty(timestamp))
{
logger.LogInformation($"Using specific timestamp: {timestamp}");
}
if (dryRun)
{
logger.LogInformation("DRY RUN: No actual changes will be made to the database");
// Perform validation here if needed
logger.LogInformation("Seed loading validation completed");
return true;
}
// Execute the load operation
seederService.LoadSeedsAsync(seedName, timestamp).GetAwaiter().GetResult();
logger.LogInformation("Seed loading completed successfully");
return true;
}
catch (Exception ex)
{
// Display the full exception details
Console.WriteLine($"Error loading seeds: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
if (ex.InnerException.InnerException != null)
{
Console.WriteLine($"Inner inner exception: {ex.InnerException.InnerException.Message}");
}
}
Console.WriteLine($"Stack trace: {ex.StackTrace}");
return false;
}
}
private void ConfigureServices(ServiceCollection services)
{
// Load configuration using the GlobalSettingsFactory
var globalSettings = GlobalSettingsFactory.GlobalSettings;
// Register services
services.AddLogging(builder => builder.AddConsole());
services.AddSingleton(globalSettings);
// Add Data Protection services
services.AddDataProtection()
.SetApplicationName("Bitwarden");
// Register DatabaseContext and services
services.AddTransient<ISeederService, SeederService>();
services.AddTransient<IDatabaseService, DatabaseService>();
services.AddTransient<IEncryptionService, EncryptionService>();
services.AddDbContext<DatabaseContext>();
}
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<key id="c9676c44-16a7-4b38-b750-4ea5443f1b87" version="1">
<creationDate>2025-03-13T17:13:50.8174933Z</creationDate>
<activationDate>2025-03-13T17:13:50.8130955Z</activationDate>
<expirationDate>2025-06-11T17:13:50.8130955Z</expirationDate>
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
<!-- Warning: the key below is in an unencrypted form. -->
<value>VCxxS1xKEWQ9+XRXgMchHML7POXcwRkCHswj7JMjjN36RkERENo+ky/1mazB4RZ6BjjwXsAyjhSz2eGts/0BnQ==</value>
</masterKey>
</descriptor>
</descriptor>
</key>

98
util/seeder/README.md Normal file
View File

@ -0,0 +1,98 @@
# Bitwarden Database Seeder
The Bitwarden Database Seeder utility is a tool for generating and loading test data for Bitwarden databases.
## Features
- Generate random user accounts with associated ciphers (passwords, secure notes, etc.)
- Load generated seeds into the database
- Configure database connections using the same configuration approach as the Bitwarden Migrator
## Usage
The seeder utility can be run in two ways:
1. Using `dotnet run` from the source:
```
cd util/DbSeederUtility
dotnet run -- generate-seeds --users 10 --ciphers 5 --output ./seeds
dotnet run -- load-seeds --path ./seeds
```
2. As a standalone executable:
```
DbSeeder.exe generate-seeds --users 10 --ciphers 5 --output ./seeds
DbSeeder.exe load-seeds --path ./seeds
```
## Commands
### Generate Seeds
Generates random seed data for users and ciphers.
```
DbSeeder.exe generate-seeds [options]
Options:
-u, --users <NUMBER> Number of users to generate (default: 10)
-c, --ciphers <NUMBER> Number of ciphers per user (default: 5)
-o, --output <DIRECTORY> Output directory for seed files (default: seeds)
```
### Load Seeds
Loads generated seed data into the database.
```
DbSeeder.exe load-seeds [options]
Options:
-p, --path <DIRECTORY> Path to the directory containing seed data
--dry-run Preview the operation without making changes
```
## Configuration
The utility uses the same configuration approach as other Bitwarden utilities:
1. **User Secrets** - For local development
2. **appsettings.json** - For general settings
3. **Environment variables** - For deployment environments
### Database Configuration
Configure the database connection in user secrets or appsettings.json:
```json
{
"globalSettings": {
"databaseProvider": "postgresql", // or "sqlserver", "mysql", "sqlite"
"postgreSql": {
"connectionString": "Host=localhost;Port=5432;Database=vault_dev;Username=postgres;Password=YOURPASSWORD;Include Error Detail=true"
},
"sqlServer": {
"connectionString": "Data Source=localhost;Initial Catalog=vault_dev;Integrated Security=SSPI;MultipleActiveResultSets=true"
}
}
}
```
## Building the Executable
To build the standalone executable:
```
cd util
.\publish-seeder.ps1
```
This will create the executable in the `util/Seeder/publish/win-x64` directory. You can also specify a different runtime:
```
.\publish-seeder.ps1 -runtime linux-x64
```
## Integration with Bitwarden Server
The seeder utility is designed to work seamlessly with Bitwarden Server. It uses the same database models and configuration approach as the server, ensuring compatibility with the core repository.

43
util/seeder/Seeder.csproj Normal file
View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Bit.Seeder</RootNamespace>
<UserSecretsId>Bit.Seeder</UserSecretsId>
<Description>Core library for generating and managing test data for Bitwarden</Description>
<OutputType>library</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Bogus" Version="35.4.0" />
<PackageReference Include="CommandDotNet" Version="7.0.3" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />
<ProjectReference Include="..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="..\..\Program.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,76 @@
using Microsoft.EntityFrameworkCore;
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
using Bit.Core.Enums;
using Bit.Seeder.Settings;
namespace Bit.Seeder.Services;
public class DatabaseContext : DbContext
{
private readonly GlobalSettings _globalSettings;
public DatabaseContext(GlobalSettings globalSettings)
{
_globalSettings = globalSettings;
}
public DbSet<User> Users { get; set; }
public DbSet<Cipher> Ciphers { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var provider = _globalSettings.DatabaseProvider ?? string.Empty;
Console.WriteLine($"Database Provider: '{provider}'");
// Output all available connection strings for debugging
Console.WriteLine($"SqlServer ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.SqlServer?.ConnectionString)}");
Console.WriteLine($"PostgreSql ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.PostgreSql?.ConnectionString)}");
Console.WriteLine($"MySql ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.MySql?.ConnectionString)}");
Console.WriteLine($"Sqlite ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.Sqlite?.ConnectionString)}");
var connectionString = _globalSettings.DatabaseProvider switch
{
"postgres" => _globalSettings.PostgreSql?.ConnectionString,
"mysql" => _globalSettings.MySql?.ConnectionString,
"sqlite" => _globalSettings.Sqlite?.ConnectionString,
_ => _globalSettings.SqlServer?.ConnectionString
};
Console.WriteLine($"Using connection string: {connectionString}");
switch (_globalSettings.DatabaseProvider)
{
case "postgres":
optionsBuilder.UseNpgsql(connectionString);
break;
case "mysql":
optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
break;
case "sqlite":
optionsBuilder.UseSqlite(connectionString);
break;
default:
optionsBuilder.UseSqlServer(connectionString);
break;
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Cipher>(entity =>
{
entity.ToTable("Cipher");
entity.Property(e => e.Type).HasConversion<int>();
entity.Property(e => e.Reprompt).HasConversion<int>();
});
modelBuilder.Entity<User>(entity =>
{
entity.ToTable("User");
entity.Property(e => e.Kdf).HasConversion<int>();
});
}
}

View File

@ -0,0 +1,85 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Seeder.Services;
public class DatabaseService : IDatabaseService
{
private readonly DatabaseContext _dbContext;
private readonly ILogger<DatabaseService> _logger;
public DatabaseService(DatabaseContext dbContext, ILogger<DatabaseService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
public async Task ClearDatabaseAsync()
{
_logger.LogInformation("Clearing database...");
var ciphers = await _dbContext.Ciphers.ToListAsync();
_dbContext.Ciphers.RemoveRange(ciphers);
var users = await _dbContext.Users.ToListAsync();
_dbContext.Users.RemoveRange(users);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Database cleared successfully.");
}
public async Task SaveUsersAsync(IEnumerable<User> users)
{
_logger.LogInformation("Saving users to database...");
foreach (var user in users)
{
await _dbContext.Users.AddAsync(user);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"Successfully saved {users.Count()} users to database.");
}
public async Task SaveCiphersAsync(IEnumerable<Cipher> ciphers)
{
_logger.LogInformation("Saving ciphers to database...");
foreach (var cipher in ciphers)
{
await _dbContext.Ciphers.AddAsync(cipher);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"Successfully saved {ciphers.Count()} ciphers to database.");
}
public async Task<List<User>> GetUsersAsync()
{
_logger.LogInformation("Retrieving all users from database...");
var users = await _dbContext.Users.ToListAsync();
_logger.LogInformation($"Successfully retrieved {users.Count} users from database.");
return users;
}
public async Task<List<Cipher>> GetCiphersAsync()
{
_logger.LogInformation("Retrieving all ciphers from database...");
var ciphers = await _dbContext.Ciphers.ToListAsync();
_logger.LogInformation($"Successfully retrieved {ciphers.Count} ciphers from database.");
return ciphers;
}
public async Task<List<Cipher>> GetCiphersByUserIdAsync(Guid userId)
{
_logger.LogInformation($"Retrieving ciphers for user {userId} from database...");
var ciphers = await _dbContext.Ciphers.Where(c => c.UserId == userId).ToListAsync();
_logger.LogInformation($"Successfully retrieved {ciphers.Count} ciphers for user {userId}.");
return ciphers;
}
}

View File

@ -0,0 +1,66 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.DataProtection;
using Bit.Core;
namespace Bit.Seeder.Services;
public class EncryptionService : IEncryptionService
{
private readonly ILogger<EncryptionService> _logger;
private readonly IDataProtector _dataProtector;
public EncryptionService(
ILogger<EncryptionService> logger,
IDataProtectionProvider dataProtectionProvider)
{
_logger = logger;
_dataProtector = dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose);
}
public string HashPassword(string password)
{
_logger.LogDebug("Hashing password using Data Protection");
// The real Bitwarden implementation uses BCrypt first and then protects that value
// For simplicity we're just protecting the raw password since this is only for seeding test data
var protectedPassword = _dataProtector.Protect(password);
// Prefix with "P|" to match Bitwarden's password format
return string.Concat(Constants.DatabaseFieldProtectedPrefix, protectedPassword);
}
public byte[] DeriveKey(string password, string salt)
{
_logger.LogDebug("Deriving key");
using var pbkdf2 = new Rfc2898DeriveBytes(
Encoding.UTF8.GetBytes(password),
Encoding.UTF8.GetBytes(salt),
100000,
HashAlgorithmName.SHA256);
return pbkdf2.GetBytes(32);
}
public string EncryptString(string plaintext, byte[] key)
{
_logger.LogDebug("Encrypting string");
using var aes = Aes.Create();
aes.Key = key;
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
var cipherBytes = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
var result = new byte[aes.IV.Length + cipherBytes.Length];
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length);
return Convert.ToBase64String(result);
}
}

View File

@ -0,0 +1,14 @@
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Seeder.Services;
public interface IDatabaseService
{
Task ClearDatabaseAsync();
Task SaveUsersAsync(IEnumerable<User> users);
Task SaveCiphersAsync(IEnumerable<Cipher> ciphers);
Task<List<User>> GetUsersAsync();
Task<List<Cipher>> GetCiphersAsync();
Task<List<Cipher>> GetCiphersByUserIdAsync(Guid userId);
}

View File

@ -0,0 +1,11 @@
using Bit.Core.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Seeder.Services;
public interface IDatabaseService
{
Task ClearDatabaseAsync();
Task SaveUsersAsync(IEnumerable<User> users);
Task SaveCiphersAsync(IEnumerable<Cipher> ciphers);
}

View File

@ -0,0 +1,8 @@
namespace Bit.Seeder.Services;
public interface IEncryptionService
{
string HashPassword(string password);
byte[] DeriveKey(string password, string salt);
string EncryptString(string plaintext, byte[] key);
}

View File

@ -0,0 +1,9 @@
namespace Bit.Seeder.Services;
public interface ISeederService
{
Task GenerateSeedsAsync(int userCount, int ciphersPerUser, string seedName);
Task LoadSeedsAsync(string seedName, string? timestamp = null);
Task GenerateAndLoadSeedsAsync(int userCount, int ciphersPerUser, string seedName);
Task ExtractSeedsAsync(string seedName);
}

View File

@ -0,0 +1,450 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bogus;
using Microsoft.Extensions.Logging;
namespace Bit.Seeder.Services;
public class SeederService : ISeederService
{
private readonly IEncryptionService _encryptionService;
private readonly IDatabaseService _databaseService;
private readonly ILogger<SeederService> _logger;
private readonly Faker _faker;
private readonly string _defaultPassword = "password";
public SeederService(
IEncryptionService encryptionService,
IDatabaseService databaseService,
ILogger<SeederService> logger)
{
_encryptionService = encryptionService;
_databaseService = databaseService;
_logger = logger;
_faker = new Faker();
// Set the random seed to ensure reproducible data
Randomizer.Seed = new Random(42);
}
public async Task GenerateSeedsAsync(int userCount, int ciphersPerUser, string outputName)
{
_logger.LogInformation("Generating seeds: {UserCount} users with {CiphersPerUser} ciphers each", userCount, ciphersPerUser);
// Create timestamped folder under a named folder in seeds directory
var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds");
Directory.CreateDirectory(seedsBaseDir);
var namedDir = Path.Combine(seedsBaseDir, outputName);
Directory.CreateDirectory(namedDir);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var outputDir = Path.Combine(namedDir, timestamp);
Directory.CreateDirectory(outputDir);
// Create users and ciphers subdirectories
Directory.CreateDirectory(Path.Combine(outputDir, "users"));
Directory.CreateDirectory(Path.Combine(outputDir, "ciphers"));
_logger.LogInformation("Seed output directory: {OutputDir}", outputDir);
// Generate users
var users = GenerateUsers(userCount);
// Generate ciphers for each user
var allCiphers = new List<Cipher>();
foreach (var user in users)
{
var ciphers = GenerateCiphers(user, ciphersPerUser);
allCiphers.AddRange(ciphers);
// Save each user's ciphers to a file
var cipherFilePath = Path.Combine(outputDir, "ciphers", $"{user.Id}.json");
await File.WriteAllTextAsync(cipherFilePath, JsonSerializer.Serialize(ciphers, new JsonSerializerOptions
{
WriteIndented = true
}));
}
// Save users to a file
var userFilePath = Path.Combine(outputDir, "users", "users.json");
await File.WriteAllTextAsync(userFilePath, JsonSerializer.Serialize(users, new JsonSerializerOptions
{
WriteIndented = true
}));
_logger.LogInformation("Successfully generated {UserCount} users and {CipherCount} ciphers", users.Count, allCiphers.Count);
_logger.LogInformation("Seed data saved to directory: {OutputDir}", outputDir);
}
public async Task GenerateAndLoadSeedsAsync(int userCount, int ciphersPerUser, string seedName)
{
_logger.LogInformation("Generating and loading seeds directly: {UserCount} users with {CiphersPerUser} ciphers each",
userCount, ciphersPerUser);
// Generate users directly without saving to files
var users = GenerateUsers(userCount);
// Clear the database first
await _databaseService.ClearDatabaseAsync();
// Save users to database
await _databaseService.SaveUsersAsync(users);
_logger.LogInformation("Saved {UserCount} users directly to database", users.Count);
// Generate and save ciphers for each user
int totalCiphers = 0;
foreach (var user in users)
{
var ciphers = GenerateCiphers(user, ciphersPerUser);
await _databaseService.SaveCiphersAsync(ciphers);
totalCiphers += ciphers.Count;
_logger.LogInformation("Saved {CipherCount} ciphers for user {UserId} directly to database",
ciphers.Count, user.Id);
}
_logger.LogInformation("Successfully generated and loaded {UserCount} users and {CipherCount} ciphers directly to database",
users.Count, totalCiphers);
}
public async Task LoadSeedsAsync(string seedName, string? timestamp = null)
{
// Construct path to seeds directory
var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds");
var namedDir = Path.Combine(seedsBaseDir, seedName);
if (!Directory.Exists(namedDir))
{
_logger.LogError("Seed directory not found: {SeedDir}", namedDir);
return;
}
string seedDir;
// If timestamp is specified, use that exact directory
if (!string.IsNullOrEmpty(timestamp))
{
seedDir = Path.Combine(namedDir, timestamp);
if (!Directory.Exists(seedDir))
{
_logger.LogError("Timestamp directory not found: {TimestampDir}", seedDir);
return;
}
}
else
{
// Otherwise, find the most recent timestamped directory
var timestampDirs = Directory.GetDirectories(namedDir);
if (timestampDirs.Length == 0)
{
_logger.LogError("No seed data found in directory: {SeedDir}", namedDir);
return;
}
// Sort by directory name (which is a timestamp) in descending order
Array.Sort(timestampDirs);
Array.Reverse(timestampDirs);
// Use the most recent one
seedDir = timestampDirs[0];
_logger.LogInformation("Using most recent seed data from: {SeedDir}", seedDir);
}
_logger.LogInformation("Loading seeds from directory: {SeedDir}", seedDir);
// Clear database first
await _databaseService.ClearDatabaseAsync();
// Load users
var userFilePath = Path.Combine(seedDir, "users", "users.json");
if (!File.Exists(userFilePath))
{
_logger.LogError("User file not found: {UserFilePath}", userFilePath);
return;
}
var userJson = await File.ReadAllTextAsync(userFilePath);
var users = JsonSerializer.Deserialize<List<User>>(userJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new List<User>();
if (users.Count == 0)
{
_logger.LogError("No users found in user file");
return;
}
// Save users to database
await _databaseService.SaveUsersAsync(users);
// Load and save ciphers for each user
var cipherDir = Path.Combine(seedDir, "ciphers");
if (!Directory.Exists(cipherDir))
{
_logger.LogError("Cipher directory not found: {CipherDir}", cipherDir);
return;
}
var cipherFiles = Directory.GetFiles(cipherDir, "*.json");
foreach (var cipherFile in cipherFiles)
{
var cipherJson = await File.ReadAllTextAsync(cipherFile);
var ciphers = JsonSerializer.Deserialize<List<Cipher>>(cipherJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new List<Cipher>();
if (ciphers.Count > 0)
{
await _databaseService.SaveCiphersAsync(ciphers);
}
}
_logger.LogInformation("Successfully loaded seed data into database");
}
public async Task ExtractSeedsAsync(string seedName)
{
_logger.LogInformation("Extracting seed data from database for seed name: {SeedName}", seedName);
// Create timestamped folder under a named folder in seeds directory
var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds");
Directory.CreateDirectory(seedsBaseDir);
var namedDir = Path.Combine(seedsBaseDir, seedName);
Directory.CreateDirectory(namedDir);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var outputDir = Path.Combine(namedDir, timestamp);
Directory.CreateDirectory(outputDir);
// Create users and ciphers subdirectories
Directory.CreateDirectory(Path.Combine(outputDir, "users"));
Directory.CreateDirectory(Path.Combine(outputDir, "ciphers"));
_logger.LogInformation("Seed output directory: {OutputDir}", outputDir);
try
{
// Get all users from the database
var users = await _databaseService.GetUsersAsync();
if (users == null || users.Count == 0)
{
_logger.LogWarning("No users found in the database");
return;
}
_logger.LogInformation("Extracted {Count} users from database", users.Count);
// Save users to a file
var userFilePath = Path.Combine(outputDir, "users", "users.json");
await File.WriteAllTextAsync(userFilePath, JsonSerializer.Serialize(users, new JsonSerializerOptions
{
WriteIndented = true
}));
int totalCiphers = 0;
// Get ciphers for each user
foreach (var user in users)
{
var ciphers = await _databaseService.GetCiphersByUserIdAsync(user.Id);
if (ciphers != null && ciphers.Count > 0)
{
// Save ciphers to a file
var cipherFilePath = Path.Combine(outputDir, "ciphers", $"{user.Id}.json");
await File.WriteAllTextAsync(cipherFilePath, JsonSerializer.Serialize(ciphers, new JsonSerializerOptions
{
WriteIndented = true
}));
totalCiphers += ciphers.Count;
}
}
_logger.LogInformation("Successfully extracted {UserCount} users and {CipherCount} ciphers", users.Count, totalCiphers);
_logger.LogInformation("Seed data saved to directory: {OutputDir}", outputDir);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting seed data: {Message}", ex.Message);
throw;
}
}
private List<User> GenerateUsers(int count)
{
_logger.LogInformation("Generating {Count} users", count);
var users = new List<User>();
for (int i = 0; i < count; i++)
{
var userId = Guid.NewGuid();
var email = _faker.Internet.Email(provider: "example.com");
var name = _faker.Name.FullName();
var masterPassword = _encryptionService.HashPassword(_defaultPassword);
var masterPasswordHint = "It's the word 'password'";
var key = _encryptionService.DeriveKey(_defaultPassword, email);
var user = new User
{
Id = userId,
Email = email,
Name = name,
MasterPassword = masterPassword,
MasterPasswordHint = masterPasswordHint,
SecurityStamp = Guid.NewGuid().ToString(),
EmailVerified = true,
ApiKey = Guid.NewGuid().ToString("N").Substring(0, 30),
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 100000,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Key = _encryptionService.EncryptString(Convert.ToBase64String(key), key)
};
users.Add(user);
}
return users;
}
private List<Cipher> GenerateCiphers(User user, int count)
{
_logger.LogInformation("Generating {Count} ciphers for user {UserId}", count, user.Id);
var ciphers = new List<Cipher>();
var key = _encryptionService.DeriveKey(_defaultPassword, user.Email);
for (int i = 0; i < count; i++)
{
var cipherId = Guid.NewGuid();
CipherType type;
string name;
string? notes = null;
var typeRandom = _faker.Random.Int(1, 4);
type = (CipherType)typeRandom;
switch (type)
{
case CipherType.Login:
name = $"Login - {_faker.Internet.DomainName()}";
var loginData = new
{
Name = name,
Notes = notes,
Username = _faker.Internet.UserName(),
Password = _faker.Internet.Password(),
Uris = new[]
{
new { Uri = $"https://{_faker.Internet.DomainName()}" }
}
};
var loginDataJson = JsonSerializer.Serialize(loginData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(loginDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
case CipherType.SecureNote:
name = $"Note - {_faker.Lorem.Word()}";
notes = _faker.Lorem.Paragraph();
var secureNoteData = new
{
Name = name,
Notes = notes,
Type = 0 // Text
};
var secureNoteDataJson = JsonSerializer.Serialize(secureNoteData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(secureNoteDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
case CipherType.Card:
name = $"Card - {_faker.Finance.CreditCardNumber().Substring(0, 4)}";
var cardData = new
{
Name = name,
Notes = notes,
CardholderName = _faker.Name.FullName(),
Number = _faker.Finance.CreditCardNumber(),
ExpMonth = _faker.Random.Int(1, 12).ToString(),
ExpYear = _faker.Random.Int(DateTime.UtcNow.Year, DateTime.UtcNow.Year + 10).ToString(),
Code = _faker.Random.Int(100, 999).ToString()
};
var cardDataJson = JsonSerializer.Serialize(cardData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(cardDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
case CipherType.Identity:
name = $"Identity - {_faker.Name.FullName()}";
var identityData = new
{
Name = name,
Notes = notes,
Title = _faker.Name.Prefix(),
FirstName = _faker.Name.FirstName(),
MiddleName = _faker.Name.FirstName(),
LastName = _faker.Name.LastName(),
Email = _faker.Internet.Email(),
Phone = _faker.Phone.PhoneNumber(),
Address1 = _faker.Address.StreetAddress(),
City = _faker.Address.City(),
State = _faker.Address.State(),
PostalCode = _faker.Address.ZipCode(),
Country = _faker.Address.CountryCode()
};
var identityDataJson = JsonSerializer.Serialize(identityData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(identityDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
}
}
return ciphers;
}
}

View File

@ -0,0 +1,383 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bogus;
using Microsoft.Extensions.Logging;
namespace Bit.Seeder.Services;
public class SeederService : ISeederService
{
private readonly IEncryptionService _encryptionService;
private readonly IDatabaseService _databaseService;
private readonly ILogger<SeederService> _logger;
private readonly Faker _faker;
private readonly string _defaultPassword = "password";
public SeederService(
IEncryptionService encryptionService,
IDatabaseService databaseService,
ILogger<SeederService> logger)
{
_encryptionService = encryptionService;
_databaseService = databaseService;
_logger = logger;
_faker = new Faker();
// Set the random seed to ensure reproducible data
Randomizer.Seed = new Random(42);
}
public async Task GenerateSeedsAsync(int userCount, int ciphersPerUser, string outputName)
{
_logger.LogInformation("Generating seeds: {UserCount} users with {CiphersPerUser} ciphers each", userCount, ciphersPerUser);
// Create timestamped folder under a named folder in seeds directory
var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds");
Directory.CreateDirectory(seedsBaseDir);
var namedDir = Path.Combine(seedsBaseDir, outputName);
Directory.CreateDirectory(namedDir);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var outputDir = Path.Combine(namedDir, timestamp);
Directory.CreateDirectory(outputDir);
// Create users and ciphers subdirectories
Directory.CreateDirectory(Path.Combine(outputDir, "users"));
Directory.CreateDirectory(Path.Combine(outputDir, "ciphers"));
_logger.LogInformation("Seed output directory: {OutputDir}", outputDir);
// Generate users
var users = GenerateUsers(userCount);
// Generate ciphers for each user
var allCiphers = new List<Cipher>();
foreach (var user in users)
{
var ciphers = GenerateCiphers(user, ciphersPerUser);
allCiphers.AddRange(ciphers);
// Save each user's ciphers to a file
var cipherFilePath = Path.Combine(outputDir, "ciphers", $"{user.Id}.json");
await File.WriteAllTextAsync(cipherFilePath, JsonSerializer.Serialize(ciphers, new JsonSerializerOptions
{
WriteIndented = true
}));
}
// Save users to a file
var userFilePath = Path.Combine(outputDir, "users", "users.json");
await File.WriteAllTextAsync(userFilePath, JsonSerializer.Serialize(users, new JsonSerializerOptions
{
WriteIndented = true
}));
_logger.LogInformation("Successfully generated {UserCount} users and {CipherCount} ciphers", users.Count, allCiphers.Count);
_logger.LogInformation("Seed data saved to directory: {OutputDir}", outputDir);
}
public async Task GenerateAndLoadSeedsAsync(int userCount, int ciphersPerUser, string seedName)
{
_logger.LogInformation("Generating and loading seeds directly: {UserCount} users with {CiphersPerUser} ciphers each",
userCount, ciphersPerUser);
// Generate users directly without saving to files
var users = GenerateUsers(userCount);
// Clear the database first
await _databaseService.ClearDatabaseAsync();
// Save users to database
await _databaseService.SaveUsersAsync(users);
_logger.LogInformation("Saved {UserCount} users directly to database", users.Count);
// Generate and save ciphers for each user
int totalCiphers = 0;
foreach (var user in users)
{
var ciphers = GenerateCiphers(user, ciphersPerUser);
await _databaseService.SaveCiphersAsync(ciphers);
totalCiphers += ciphers.Count;
_logger.LogInformation("Saved {CipherCount} ciphers for user {UserId} directly to database",
ciphers.Count, user.Id);
}
_logger.LogInformation("Successfully generated and loaded {UserCount} users and {CipherCount} ciphers directly to database",
users.Count, totalCiphers);
}
public async Task LoadSeedsAsync(string seedName, string? timestamp = null)
{
// Construct path to seeds directory
var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds");
var namedDir = Path.Combine(seedsBaseDir, seedName);
if (!Directory.Exists(namedDir))
{
_logger.LogError("Seed directory not found: {SeedDir}", namedDir);
return;
}
string seedDir;
// If timestamp is specified, use that exact directory
if (!string.IsNullOrEmpty(timestamp))
{
seedDir = Path.Combine(namedDir, timestamp);
if (!Directory.Exists(seedDir))
{
_logger.LogError("Timestamp directory not found: {TimestampDir}", seedDir);
return;
}
}
else
{
// Otherwise, find the most recent timestamped directory
var timestampDirs = Directory.GetDirectories(namedDir);
if (timestampDirs.Length == 0)
{
_logger.LogError("No seed data found in directory: {SeedDir}", namedDir);
return;
}
// Sort by directory name (which is a timestamp) in descending order
Array.Sort(timestampDirs);
Array.Reverse(timestampDirs);
// Use the most recent one
seedDir = timestampDirs[0];
_logger.LogInformation("Using most recent seed data from: {SeedDir}", seedDir);
}
_logger.LogInformation("Loading seeds from directory: {SeedDir}", seedDir);
// Clear database first
await _databaseService.ClearDatabaseAsync();
// Load users
var userFilePath = Path.Combine(seedDir, "users", "users.json");
if (!File.Exists(userFilePath))
{
_logger.LogError("User file not found: {UserFilePath}", userFilePath);
return;
}
var userJson = await File.ReadAllTextAsync(userFilePath);
var users = JsonSerializer.Deserialize<List<User>>(userJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new List<User>();
if (users.Count == 0)
{
_logger.LogError("No users found in user file");
return;
}
// Save users to database
await _databaseService.SaveUsersAsync(users);
// Load and save ciphers for each user
var cipherDir = Path.Combine(seedDir, "ciphers");
if (!Directory.Exists(cipherDir))
{
_logger.LogError("Cipher directory not found: {CipherDir}", cipherDir);
return;
}
var cipherFiles = Directory.GetFiles(cipherDir, "*.json");
foreach (var cipherFile in cipherFiles)
{
var cipherJson = await File.ReadAllTextAsync(cipherFile);
var ciphers = JsonSerializer.Deserialize<List<Cipher>>(cipherJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new List<Cipher>();
if (ciphers.Count > 0)
{
await _databaseService.SaveCiphersAsync(ciphers);
}
}
_logger.LogInformation("Successfully loaded seed data into database");
}
private List<User> GenerateUsers(int count)
{
_logger.LogInformation("Generating {Count} users", count);
var users = new List<User>();
for (int i = 0; i < count; i++)
{
var userId = Guid.NewGuid();
var email = _faker.Internet.Email(provider: "example.com");
var name = _faker.Name.FullName();
var masterPassword = _encryptionService.HashPassword(_defaultPassword);
var masterPasswordHint = "It's the word 'password'";
var key = _encryptionService.DeriveKey(_defaultPassword, email);
var user = new User
{
Id = userId,
Email = email,
Name = name,
MasterPassword = masterPassword,
MasterPasswordHint = masterPasswordHint,
SecurityStamp = Guid.NewGuid().ToString(),
EmailVerified = true,
ApiKey = Guid.NewGuid().ToString("N").Substring(0, 30),
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 100000,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Key = _encryptionService.EncryptString(Convert.ToBase64String(key), key)
};
users.Add(user);
}
return users;
}
private List<Cipher> GenerateCiphers(User user, int count)
{
_logger.LogInformation("Generating {Count} ciphers for user {UserId}", count, user.Id);
var ciphers = new List<Cipher>();
var key = _encryptionService.DeriveKey(_defaultPassword, user.Email);
for (int i = 0; i < count; i++)
{
var cipherId = Guid.NewGuid();
CipherType type;
string name;
string? notes = null;
var typeRandom = _faker.Random.Int(1, 4);
type = (CipherType)typeRandom;
switch (type)
{
case CipherType.Login:
name = $"Login - {_faker.Internet.DomainName()}";
var loginData = new
{
Name = name,
Notes = notes,
Username = _faker.Internet.UserName(),
Password = _faker.Internet.Password(),
Uris = new[]
{
new { Uri = $"https://{_faker.Internet.DomainName()}" }
}
};
var loginDataJson = JsonSerializer.Serialize(loginData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(loginDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
case CipherType.SecureNote:
name = $"Note - {_faker.Lorem.Word()}";
notes = _faker.Lorem.Paragraph();
var secureNoteData = new
{
Name = name,
Notes = notes,
Type = 0 // Text
};
var secureNoteDataJson = JsonSerializer.Serialize(secureNoteData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(secureNoteDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
case CipherType.Card:
name = $"Card - {_faker.Finance.CreditCardNumber().Substring(0, 4)}";
var cardData = new
{
Name = name,
Notes = notes,
CardholderName = _faker.Name.FullName(),
Number = _faker.Finance.CreditCardNumber(),
ExpMonth = _faker.Random.Int(1, 12).ToString(),
ExpYear = _faker.Random.Int(DateTime.UtcNow.Year, DateTime.UtcNow.Year + 10).ToString(),
Code = _faker.Random.Int(100, 999).ToString()
};
var cardDataJson = JsonSerializer.Serialize(cardData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(cardDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
case CipherType.Identity:
name = $"Identity - {_faker.Name.FullName()}";
var identityData = new
{
Name = name,
Notes = notes,
Title = _faker.Name.Prefix(),
FirstName = _faker.Name.FirstName(),
MiddleName = _faker.Name.FirstName(),
LastName = _faker.Name.LastName(),
Email = _faker.Internet.Email(),
Phone = _faker.Phone.PhoneNumber(),
Address1 = _faker.Address.StreetAddress(),
City = _faker.Address.City(),
State = _faker.Address.State(),
PostalCode = _faker.Address.ZipCode(),
Country = _faker.Address.CountryCode()
};
var identityDataJson = JsonSerializer.Serialize(identityData);
ciphers.Add(new Cipher
{
Id = cipherId,
UserId = user.Id,
Type = type,
Data = _encryptionService.EncryptString(identityDataJson, key),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Reprompt = CipherRepromptType.None
});
break;
}
}
return ciphers;
}
}

View File

@ -0,0 +1,16 @@
namespace Bit.Seeder.Settings;
public class GlobalSettings
{
public bool SelfHosted { get; set; }
public string DatabaseProvider { get; set; } = string.Empty;
public SqlSettings SqlServer { get; set; } = new SqlSettings();
public SqlSettings PostgreSql { get; set; } = new SqlSettings();
public SqlSettings MySql { get; set; } = new SqlSettings();
public SqlSettings Sqlite { get; set; } = new SqlSettings();
public class SqlSettings
{
public string ConnectionString { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,86 @@
using Microsoft.Extensions.Configuration;
namespace Bit.Seeder.Settings;
public static class GlobalSettingsFactory
{
private static GlobalSettings? _globalSettings;
public static GlobalSettings GlobalSettings
{
get
{
if (_globalSettings == null)
{
_globalSettings = LoadGlobalSettings();
}
return _globalSettings;
}
}
private static GlobalSettings LoadGlobalSettings()
{
Console.WriteLine("Loading global settings...");
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true)
.AddUserSecrets("bitwarden-Api") // Load user secrets from the API project
.AddEnvironmentVariables();
var configuration = configBuilder.Build();
var globalSettingsSection = configuration.GetSection("globalSettings");
// Debug: Print all settings from globalSettings section
foreach (var setting in globalSettingsSection.GetChildren())
{
Console.WriteLine($"Found setting: {setting.Key}");
foreach (var child in setting.GetChildren())
{
Console.WriteLine($" - {setting.Key}.{child.Key}");
}
}
var settings = new GlobalSettings();
globalSettingsSection.Bind(settings);
// Output the loaded settings
Console.WriteLine($"Loaded DatabaseProvider: {settings.DatabaseProvider}");
Console.WriteLine($"PostgreSql settings loaded: {settings.PostgreSql != null}");
Console.WriteLine($"SqlServer settings loaded: {settings.SqlServer != null}");
Console.WriteLine($"MySql settings loaded: {settings.MySql != null}");
Console.WriteLine($"Sqlite settings loaded: {settings.Sqlite != null}");
// Check for case sensitivity issue with PostgreSql/postgresql keys
var postgresqlValue = globalSettingsSection.GetSection("postgresql")?.Value;
var postgreSqlValue = globalSettingsSection.GetSection("postgreSql")?.Value;
Console.WriteLine($"Raw check - postgresql setting exists: {postgresqlValue != null}");
Console.WriteLine($"Raw check - postgreSql setting exists: {postgreSqlValue != null}");
return settings;
}
}
// Non-static version that can accept command-line arguments
public class GlobalSettingsFactoryWithArgs
{
public GlobalSettings GlobalSettings { get; }
public GlobalSettingsFactoryWithArgs(string[] args)
{
GlobalSettings = new GlobalSettings();
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true)
.AddUserSecrets("bitwarden-Api")
.AddCommandLine(args)
.AddEnvironmentVariables()
.Build();
config.GetSection("globalSettings").Bind(GlobalSettings);
}
}

View File

@ -0,0 +1,18 @@
{
"globalSettings": {
"selfHosted": true,
"databaseProvider": "postgres",
"sqlServer": {
"connectionString": "Server=localhost;Database=vault_dev;User Id=SA;Password=Th3P455word?;Encrypt=True;TrustServerCertificate=True"
},
"postgreSql": {
"connectionString": "Host=localhost;Username=postgres;Password=Th3P455word?;Database=vault_dev;Include Error Detail=true"
},
"mysql": {
"connectionString": "server=localhost;uid=root;pwd=Th3P455word?;database=vault_dev"
},
"sqlite": {
"connectionString": "Data Source=./bitwarden_test.sqlite"
}
}
}