diff --git a/entries/bitwarden.go b/entries/bitwarden.go index d7e8cb3..24f9f10 100644 --- a/entries/bitwarden.go +++ b/entries/bitwarden.go @@ -1,11 +1,11 @@ package entries -import "time" - const bitwardenLoginItemType = 1 -// BitwardenExport represents Bitwarden's plaintext JSON vault export format. +// BitwardenExport represents Bitwarden's portable plaintext JSON format. // MaxWarden does not have folders, so every item is exported at the vault root. +// Server-owned metadata such as IDs and revision dates is deliberately omitted; +// Bitwarden creates those values when importing into the destination vault. type BitwardenExport struct { Encrypted bool `json:"encrypted"` Folders []any `json:"folders"` @@ -13,20 +13,12 @@ type BitwardenExport struct { } type BitwardenItem struct { - PasswordHistory []any `json:"passwordHistory"` - RevisionDate string `json:"revisionDate"` - CreationDate string `json:"creationDate"` - DeletedDate *string `json:"deletedDate"` - ID string `json:"id"` - OrganizationID *string `json:"organizationId"` - FolderID *string `json:"folderId"` - Type int `json:"type"` - Reprompt int `json:"reprompt"` - Name string `json:"name"` - Notes string `json:"notes"` - Favorite bool `json:"favorite"` - Login BitwardenLogin `json:"login"` - CollectionIDs []string `json:"collectionIds"` + Type int `json:"type"` + Reprompt int `json:"reprompt"` + Name string `json:"name"` + Notes string `json:"notes"` + Favorite bool `json:"favorite"` + Login BitwardenLogin `json:"login"` } type BitwardenLogin struct { @@ -51,21 +43,16 @@ func NewBitwardenExport(secrets []Secret) BitwardenExport { } items = append(items, BitwardenItem{ - PasswordHistory: nil, - RevisionDate: bitwardenTime(secret.Modified), - CreationDate: bitwardenTime(secret.Created), - ID: secret.ID, - Type: bitwardenLoginItemType, - Reprompt: 0, - Name: secret.Description, - Notes: secret.Notes, - Favorite: false, + Type: bitwardenLoginItemType, + Reprompt: 0, + Name: secret.Description, + Notes: secret.Notes, + Favorite: false, Login: BitwardenLogin{ URIs: uris, Username: secret.Username, Password: secret.Password, }, - CollectionIDs: nil, }) } @@ -75,11 +62,3 @@ func NewBitwardenExport(secrets []Secret) BitwardenExport { Items: items, } } - -func bitwardenTime(value time.Time) string { - if value.IsZero() { - return "" - } - - return value.UTC().Format(time.RFC3339Nano) -} diff --git a/entries/bitwarden_test.go b/entries/bitwarden_test.go index ac97d81..b301e6f 100644 --- a/entries/bitwarden_test.go +++ b/entries/bitwarden_test.go @@ -3,13 +3,9 @@ package entries import ( "encoding/json" "testing" - "time" ) func TestNewBitwardenExport(t *testing.T) { - created := time.Date(2026, time.September, 3, 12, 30, 0, 0, time.FixedZone("EDT", -4*60*60)) - modified := created.Add(15 * time.Minute) - export := NewBitwardenExport([]Secret{{ ID: "c381ca2b-0f53-4b05-b209-f2eb11f05e19", Description: "Example", @@ -17,8 +13,6 @@ func TestNewBitwardenExport(t *testing.T) { Notes: "A note", Username: "person@example.com", Password: "correct horse battery staple", - Created: created, - Modified: modified, }}) if export.Encrypted { @@ -44,10 +38,6 @@ func TestNewBitwardenExport(t *testing.T) { if len(item.Login.URIs) != 1 || item.Login.URIs[0].URI != "https://example.com/login" { t.Fatalf("login URI was not mapped: %#v", item.Login.URIs) } - if item.CreationDate != "2026-09-03T16:30:00Z" || item.RevisionDate != "2026-09-03T16:45:00Z" { - t.Fatalf("timestamps were not normalized to UTC: %#v", item) - } - encoded, err := json.Marshal(export) if err != nil { t.Fatalf("marshal export: %v", err) @@ -66,16 +56,20 @@ func TestNewBitwardenExport(t *testing.T) { if _, ok := document["items"]; !ok { t.Fatal("export is missing items field") } + + encodedItem := document["items"].([]any)[0].(map[string]any) + for _, field := range []string{"id", "organizationId", "folderId", "revisionDate", "creationDate", "deletedDate", "passwordHistory", "collectionIds"} { + if _, ok := encodedItem[field]; ok { + t.Fatalf("import-only export must not include server-owned field %q", field) + } + } } -func TestNewBitwardenExportWithoutURLOrDates(t *testing.T) { +func TestNewBitwardenExportWithoutURL(t *testing.T) { export := NewBitwardenExport([]Secret{{Description: "No URL"}}) item := export.Items[0] if item.Login.URIs == nil || len(item.Login.URIs) != 0 { t.Fatalf("expected an empty URI array, got %#v", item.Login.URIs) } - if item.CreationDate != "" || item.RevisionDate != "" { - t.Fatalf("zero timestamps should be empty strings: %#v", item) - } }