using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Seeder.Data; using Bit.Seeder.Data.Distributions; using Bit.Seeder.Data.Enums; using Bit.Seeder.Data.Generators; using Bit.Seeder.Data.Static; using Bit.Seeder.Models; namespace Bit.Seeder.Factories; /// /// Composes cipher entities from generated data, handling encryption and ownership assignment. /// Used by generation steps to create realistic ciphers for organizations or personal vaults. /// internal static class CipherComposer { internal static Cipher Compose( int index, CipherType cipherType, string encryptionKey, Company[] companies, GeneratorContext generator, Distribution passwordDistribution, Guid? organizationId = null, Guid? userId = null, CipherRepromptType reprompt = CipherRepromptType.None) { return cipherType switch { CipherType.Login => ComposeLogin(index, encryptionKey, companies, generator, passwordDistribution, organizationId, userId, reprompt), CipherType.Card => ComposeCard(index, encryptionKey, generator, organizationId, userId, reprompt), CipherType.Identity => ComposeIdentity(index, encryptionKey, generator, organizationId, userId, reprompt), CipherType.SecureNote => ComposeSecureNote(index, encryptionKey, generator, organizationId, userId, reprompt), CipherType.SSHKey => ComposeSshKey(index, encryptionKey, organizationId, userId, reprompt), _ => throw new ArgumentException($"Unsupported cipher type: {cipherType}") }; } private static Cipher ComposeLogin( int index, string encryptionKey, Company[] companies, GeneratorContext generator, Distribution passwordDistribution, Guid? organizationId = null, Guid? userId = null, CipherRepromptType reprompt = CipherRepromptType.None) { var company = companies[index % companies.Length]; var uri = $"https://{company.Domain}"; var username = generator.Username.GenerateByIndex(index, totalHint: generator.CipherCount, domain: company.Domain); // ~20% of logins get a FIDO2 passkey; ~40% get a 1-3 entry password history. Deterministic by index. var fido2Credentials = index % 5 == 0 ? new List { LoginCipherSeeder.CreateFido2Credential(company.Domain, company.Name, username) } : null; var passwordHistory = index % 5 < 2 ? BuildPasswordHistory(index, 1 + (index % 3), generator.CipherCount, passwordDistribution) : null; return LoginCipherSeeder.Create(new CipherSeed { Type = CipherType.Login, Name = $"{company.Name} ({company.Category})", EncryptionKey = encryptionKey, OrganizationId = organizationId, UserId = userId, Reprompt = reprompt, Login = new LoginViewDto { Username = username, Password = Passwords.GetPassword(index, generator.CipherCount, passwordDistribution), Uris = [new LoginUriViewDto { Uri = uri }], Fido2Credentials = fido2Credentials, PasswordHistory = passwordHistory } }); } internal static List BuildPasswordHistory( int index, int entryCount, int total, Distribution passwordDistribution) { var history = new List(entryCount); for (var k = 1; k <= entryCount; k++) { // Walk to a deterministic prior position in the pool. The offset must stay in [0, total) so // Distribution.Select lands in a real bucket (otherwise every historical password collapses into the // strongest tier), and must be non-zero so priorIndex != index — i.e. the prior password is genuinely // distinct from the current one. 7919 is prime; multiplying by k varies offsets across entries. int priorIndex; if (total <= 1) { priorIndex = 0; } else { var offset = (k * 7919) % total; if (offset == 0) { offset = 1; } priorIndex = (index + offset) % total; } history.Add(new PasswordHistoryViewDto { Password = Passwords.GetPassword(priorIndex, total, passwordDistribution), LastUsedDate = DateTime.UtcNow.AddDays(-7 * k) }); } return history; } private static Cipher ComposeCard( int index, string encryptionKey, GeneratorContext generator, Guid? organizationId = null, Guid? userId = null, CipherRepromptType reprompt = CipherRepromptType.None) { var card = generator.Card.GenerateByIndex(index); return CardCipherSeeder.Create(new CipherSeed { Type = CipherType.Card, Name = $"{card.CardholderName}'s {card.Brand}", EncryptionKey = encryptionKey, OrganizationId = organizationId, UserId = userId, Reprompt = reprompt, Card = card }); } private static Cipher ComposeIdentity( int index, string encryptionKey, GeneratorContext generator, Guid? organizationId = null, Guid? userId = null, CipherRepromptType reprompt = CipherRepromptType.None) { var identity = generator.Identity.GenerateByIndex(index); var name = $"{identity.FirstName} {identity.LastName}"; if (!string.IsNullOrEmpty(identity.Company)) { name += $" ({identity.Company})"; } return IdentityCipherSeeder.Create(new CipherSeed { Type = CipherType.Identity, Name = name, EncryptionKey = encryptionKey, OrganizationId = organizationId, UserId = userId, Reprompt = reprompt, Identity = identity }); } private static Cipher ComposeSecureNote( int index, string encryptionKey, GeneratorContext generator, Guid? organizationId = null, Guid? userId = null, CipherRepromptType reprompt = CipherRepromptType.None) { var (name, notes) = generator.SecureNote.GenerateByIndex(index); return SecureNoteCipherSeeder.Create(new CipherSeed { Type = CipherType.SecureNote, Name = name, Notes = notes, EncryptionKey = encryptionKey, OrganizationId = organizationId, UserId = userId, Reprompt = reprompt }); } private static Cipher ComposeSshKey( int index, string encryptionKey, Guid? organizationId = null, Guid? userId = null, CipherRepromptType reprompt = CipherRepromptType.None) { var sshKey = SshKeyDataGenerator.GenerateByIndex(index); return SshKeyCipherSeeder.Create(new CipherSeed { Type = CipherType.SSHKey, Name = $"SSH Key {index + 1}", EncryptionKey = encryptionKey, OrganizationId = organizationId, UserId = userId, Reprompt = reprompt, SshKey = sshKey }); } /// /// Assigns a folder to a cipher via round-robin selection from the user's folder list. /// internal static void AssignFolder(Cipher cipher, Guid userId, int index, Dictionary> userFolderIds) { if (userFolderIds.TryGetValue(userId, out var folderIds) && folderIds.Count > 0) { cipher.Folders = $"{{\"{userId.ToString().ToUpperInvariant()}\":\"{folderIds[index % folderIds.Count].ToString().ToUpperInvariant()}\"}}"; } } /// /// Builds the Folders JSON column value from a set of (userId, folderId) pairs. /// Produces {"USERID1":"FOLDERID1","USERID2":"FOLDERID2"} with uppercase GUIDs. /// internal static string BuildFoldersJson(Dictionary userFolderMap) { var entries = userFolderMap.Select(kvp => $"\"{kvp.Key.ToString().ToUpperInvariant()}\":\"{kvp.Value.ToString().ToUpperInvariant()}\""); return $"{{{string.Join(",", entries)}}}"; } /// /// Builds the Favorites JSON column value from a set of user IDs. /// Produces {"USERID1":true,"USERID2":true} with uppercase GUIDs. /// internal static string BuildFavoritesJson(List userIds) { var entries = userIds.Select(id => $"\"{id.ToString().ToUpperInvariant()}\":true"); return $"{{{string.Join(",", entries)}}}"; } }