Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 66 additions & 27 deletions test/SeederApi.IntegrationTest/RustSdkCipherTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ο»Ώusing System.Text.Json;
using System.Text.Json.Serialization;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Models.Data;
using Bit.RustSDK;
using Bit.Seeder.Attributes;
Expand Down Expand Up @@ -158,14 +159,20 @@ public void CipherSeeder_ProducesServerCompatibleFormat()
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var orgId = Guid.NewGuid();

var cipher = LoginCipherSeeder.Create(
orgKeys.Key,
name: "GitHub Account",
organizationId: orgId,
username: "developer@example.com",
password: "SecureP@ss123!",
uri: "https://github.com",
notes: "My development account");
var cipher = LoginCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Login,
Name = "GitHub Account",
EncryptionKey = orgKeys.Key,
OrganizationId = orgId,
Notes = "My development account",
Login = new LoginViewDto
{
Username = "developer@example.com",
Password = "SecureP@ss123!",
Uris = [new LoginUriViewDto { Uri = "https://github.com" }]
}
});

Assert.Equal(orgId, cipher.OrganizationId);
Assert.Null(cipher.UserId);
Expand Down Expand Up @@ -195,17 +202,24 @@ public void CipherSeeder_WithFields_ProducesCorrectServerFormat()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();

var cipher = LoginCipherSeeder.Create(
orgKeys.Key,
name: "API Service",
organizationId: Guid.NewGuid(),
username: "service@example.com",
password: "SvcP@ss!",
uri: "https://api.example.com",
fields: [
("API Key", "sk_test_FAKE_abc123", 1),
("Environment", "production", 0)
]);
var cipher = LoginCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Login,
Name = "API Service",
EncryptionKey = orgKeys.Key,
OrganizationId = Guid.NewGuid(),
Login = new LoginViewDto
{
Username = "service@example.com",
Password = "SvcP@ss!",
Uris = [new LoginUriViewDto { Uri = "https://api.example.com" }]
},
Fields =
[
new FieldViewDto { Name = "API Key", Value = "sk_test_FAKE_abc123", Type = 1 },
new FieldViewDto { Name = "Environment", Value = "production", Type = 0 }
]
});

var loginData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);
Assert.NotNull(loginData);
Expand Down Expand Up @@ -345,7 +359,15 @@ public void CipherSeeder_CardCipher_ProducesServerCompatibleFormat()
Code = "456"
};

var cipher = CardCipherSeeder.Create(orgKeys.Key, name: "Business Card", card: card, organizationId: orgId, notes: "Company expenses");
var cipher = CardCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Card,
Name = "Business Card",
Notes = "Company expenses",
EncryptionKey = orgKeys.Key,
OrganizationId = orgId,
Card = card
});

Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.Card, cipher.Type);
Expand Down Expand Up @@ -379,7 +401,14 @@ public void CipherSeeder_IdentityCipher_ProducesServerCompatibleFormat()
PassportNumber = "X12345678"
};

var cipher = IdentityCipherSeeder.Create(orgKeys.Key, name: "Dr. Alice Johnson", identity: identity, organizationId: orgId);
var cipher = IdentityCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Identity,
Name = "Dr. Alice Johnson",
EncryptionKey = orgKeys.Key,
OrganizationId = orgId,
Identity = identity
});

Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.Identity, cipher.Type);
Expand All @@ -402,11 +431,14 @@ public void CipherSeeder_SecureNoteCipher_ProducesServerCompatibleFormat()
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var orgId = Guid.NewGuid();

var cipher = SecureNoteCipherSeeder.Create(
orgKeys.Key,
name: "Production Secrets",
organizationId: orgId,
notes: "DATABASE_URL=postgres://user:FAKE_secret@db.example.com/prod");
var cipher = SecureNoteCipherSeeder.Create(new CipherSeed
{
Type = CipherType.SecureNote,
Name = "Production Secrets",
Notes = "DATABASE_URL=postgres://user:FAKE_secret@db.example.com/prod",
EncryptionKey = orgKeys.Key,
OrganizationId = orgId
});

Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.SecureNote, cipher.Type);
Expand Down Expand Up @@ -435,7 +467,14 @@ public void CipherSeeder_SshKeyCipher_ProducesServerCompatibleFormat()
Fingerprint = "SHA256:examplefingerprint123"
};

var cipher = SshKeyCipherSeeder.Create(orgKeys.Key, name: "Production Deploy Key", sshKey: sshKey, organizationId: orgId);
var cipher = SshKeyCipherSeeder.Create(new CipherSeed
{
Type = CipherType.SSHKey,
Name = "Production Deploy Key",
EncryptionKey = orgKeys.Key,
OrganizationId = orgId,
SshKey = sshKey
});

Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.SSHKey, cipher.Type);
Expand Down
9 changes: 8 additions & 1 deletion util/Seeder/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dotnet test test/SeederApi.IntegrationTest/ --filter "FullyQualifiedName~TestMet
```
Need to create test data?
β”œβ”€ ONE entity with encryption? β†’ Factory
β”œβ”€ ONE cipher from a SeedVaultItem? β†’ CipherSeed.FromSeedItem() + {Type}CipherSeeder.Create()
β”œβ”€ MANY entities as cohesive operation? β†’ Recipe or Pipeline
β”œβ”€ Flexible preset-based seeding? β†’ Pipeline (RecipeBuilder + Steps)
β”œβ”€ Complete test scenario with ID mangling? β†’ Scene
Expand Down Expand Up @@ -122,11 +123,17 @@ The Seeder uses the Rust SDK via FFI because it must behave like a real Bitwarde

## Data Flow

### Pipeline path (fixture β†’ entity)
```
SeedVaultItem β†’ CipherSeed.FromSeedItem() β†’ CipherSeed β†’ {Type}CipherSeeder.Create(options) β†’ CipherViewDto β†’ encrypt_fields (Rust FFI) β†’ EncryptedCipherDto β†’ EncryptedCipherDtoExtensions β†’ Server Cipher Entity
```

### Core encryption (shared by all paths)
```
CipherViewDto β†’ JSON + [EncryptProperty] field paths β†’ encrypt_fields (Rust FFI, bitwarden_crypto) β†’ EncryptedCipherDto β†’ EncryptedCipherDtoExtensions β†’ Server Cipher Entity
```

Shared logic: `CipherEncryption.cs`, `EncryptedCipherDtoExtensions.cs`
Shared logic: `Factories/CipherEncryption.cs`, `Models/EncryptedCipherDtoExtensions.cs`

## Rust Crypto Dependency

Expand Down
49 changes: 9 additions & 40 deletions util/Seeder/Factories/CardCipherSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,21 @@ namespace Bit.Seeder.Factories;

internal static class CardCipherSeeder
{
internal static Cipher Create(
string encryptionKey,
string name,
CardViewDto card,
Guid? organizationId = null,
Guid? userId = null,
string? notes = null)
internal static Cipher Create(CipherSeed options)
{
var cipherView = new CipherViewDto
{
OrganizationId = organizationId,
Name = name,
Notes = notes,
Type = CipherTypes.Card,
Card = card
};

var encrypted = CipherEncryption.Encrypt(cipherView, encryptionKey);
return CipherEncryption.CreateEntity(encrypted, encrypted.ToCardData(), CipherType.Card, organizationId, userId);
}

internal static Cipher CreateFromSeed(
string encryptionKey,
SeedVaultItem item,
Guid? organizationId = null,
Guid? userId = null)
{
var cipherView = new CipherViewDto
{
OrganizationId = organizationId,
Name = item.Name,
Notes = item.Notes,
OrganizationId = options.OrganizationId,
Name = options.Name,
Notes = options.Notes,
Type = CipherTypes.Card,
Card = item.Card == null ? null : new CardViewDto
{
CardholderName = item.Card.CardholderName,
Brand = item.Card.Brand,
Number = item.Card.Number,
ExpMonth = item.Card.ExpMonth,
ExpYear = item.Card.ExpYear,
Code = item.Card.Code
},
Fields = SeedItemMapping.MapFields(item.Fields)
Card = options.Card,
Fields = options.Fields
};

var encrypted = CipherEncryption.Encrypt(cipherView, encryptionKey);
return CipherEncryption.CreateEntity(encrypted, encrypted.ToCardData(), CipherType.Card, organizationId, userId);
var encrypted = CipherEncryption.Encrypt(cipherView, options.EncryptionKey!);
return CipherEncryption.CreateEntity(encrypted, encrypted.ToCardData(), CipherType.Card, options.OrganizationId, options.UserId);
}

}
84 changes: 52 additions & 32 deletions util/Seeder/Factories/CipherComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Bit.Seeder.Data.Enums;
using Bit.Seeder.Data.Generators;
using Bit.Seeder.Data.Static;
using Bit.Seeder.Models;

namespace Bit.Seeder.Factories;

Expand Down Expand Up @@ -45,14 +46,21 @@ private static Cipher ComposeLogin(
Guid? userId = null)
{
var company = companies[index % companies.Length];
return LoginCipherSeeder.Create(
encryptionKey,
name: $"{company.Name} ({company.Category})",
organizationId: organizationId,
userId: userId,
username: generator.Username.GenerateByIndex(index, totalHint: generator.CipherCount, domain: company.Domain),
password: Passwords.GetPassword(index, generator.CipherCount, passwordDistribution),
uri: $"https://{company.Domain}");
var uri = $"https://{company.Domain}";
return LoginCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Login,
Name = $"{company.Name} ({company.Category})",
EncryptionKey = encryptionKey,
OrganizationId = organizationId,
UserId = userId,
Login = new LoginViewDto
{
Username = generator.Username.GenerateByIndex(index, totalHint: generator.CipherCount, domain: company.Domain),
Password = Passwords.GetPassword(index, generator.CipherCount, passwordDistribution),
Uris = [new LoginUriViewDto { Uri = uri }]
}
});
}

private static Cipher ComposeCard(
Expand All @@ -63,12 +71,15 @@ private static Cipher ComposeCard(
Guid? userId = null)
{
var card = generator.Card.GenerateByIndex(index);
return CardCipherSeeder.Create(
encryptionKey,
name: $"{card.CardholderName}'s {card.Brand}",
card: card,
organizationId: organizationId,
userId: userId);
return CardCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Card,
Name = $"{card.CardholderName}'s {card.Brand}",
EncryptionKey = encryptionKey,
OrganizationId = organizationId,
UserId = userId,
Card = card
});
}

private static Cipher ComposeIdentity(
Expand All @@ -84,12 +95,15 @@ private static Cipher ComposeIdentity(
{
name += $" ({identity.Company})";
}
return IdentityCipherSeeder.Create(
encryptionKey,
name: name,
identity: identity,
organizationId: organizationId,
userId: userId);
return IdentityCipherSeeder.Create(new CipherSeed
{
Type = CipherType.Identity,
Name = name,
EncryptionKey = encryptionKey,
OrganizationId = organizationId,
UserId = userId,
Identity = identity
});
}

private static Cipher ComposeSecureNote(
Expand All @@ -100,12 +114,15 @@ private static Cipher ComposeSecureNote(
Guid? userId = null)
{
var (name, notes) = generator.SecureNote.GenerateByIndex(index);
return SecureNoteCipherSeeder.Create(
encryptionKey,
name: name,
organizationId: organizationId,
userId: userId,
notes: notes);
return SecureNoteCipherSeeder.Create(new CipherSeed
{
Type = CipherType.SecureNote,
Name = name,
Notes = notes,
EncryptionKey = encryptionKey,
OrganizationId = organizationId,
UserId = userId
});
}

private static Cipher ComposeSshKey(
Expand All @@ -115,12 +132,15 @@ private static Cipher ComposeSshKey(
Guid? userId = null)
{
var sshKey = SshKeyDataGenerator.GenerateByIndex(index);
return SshKeyCipherSeeder.Create(
encryptionKey,
name: $"SSH Key {index + 1}",
sshKey: sshKey,
organizationId: organizationId,
userId: userId);
return SshKeyCipherSeeder.Create(new CipherSeed
{
Type = CipherType.SSHKey,
Name = $"SSH Key {index + 1}",
EncryptionKey = encryptionKey,
OrganizationId = organizationId,
UserId = userId,
SshKey = sshKey
});
}

/// <summary>
Expand Down
Loading
Loading