fix export

This commit is contained in:
2026-09-03 11:26:47 -04:00
parent 7c04370e48
commit 7c886a7927
2 changed files with 22 additions and 49 deletions

View File

@@ -1,11 +1,11 @@
package entries package entries
import "time"
const bitwardenLoginItemType = 1 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. // 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 { type BitwardenExport struct {
Encrypted bool `json:"encrypted"` Encrypted bool `json:"encrypted"`
Folders []any `json:"folders"` Folders []any `json:"folders"`
@@ -13,20 +13,12 @@ type BitwardenExport struct {
} }
type BitwardenItem 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"` Type int `json:"type"`
Reprompt int `json:"reprompt"` Reprompt int `json:"reprompt"`
Name string `json:"name"` Name string `json:"name"`
Notes string `json:"notes"` Notes string `json:"notes"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
Login BitwardenLogin `json:"login"` Login BitwardenLogin `json:"login"`
CollectionIDs []string `json:"collectionIds"`
} }
type BitwardenLogin struct { type BitwardenLogin struct {
@@ -51,10 +43,6 @@ func NewBitwardenExport(secrets []Secret) BitwardenExport {
} }
items = append(items, BitwardenItem{ items = append(items, BitwardenItem{
PasswordHistory: nil,
RevisionDate: bitwardenTime(secret.Modified),
CreationDate: bitwardenTime(secret.Created),
ID: secret.ID,
Type: bitwardenLoginItemType, Type: bitwardenLoginItemType,
Reprompt: 0, Reprompt: 0,
Name: secret.Description, Name: secret.Description,
@@ -65,7 +53,6 @@ func NewBitwardenExport(secrets []Secret) BitwardenExport {
Username: secret.Username, Username: secret.Username,
Password: secret.Password, Password: secret.Password,
}, },
CollectionIDs: nil,
}) })
} }
@@ -75,11 +62,3 @@ func NewBitwardenExport(secrets []Secret) BitwardenExport {
Items: items, Items: items,
} }
} }
func bitwardenTime(value time.Time) string {
if value.IsZero() {
return ""
}
return value.UTC().Format(time.RFC3339Nano)
}

View File

@@ -3,13 +3,9 @@ package entries
import ( import (
"encoding/json" "encoding/json"
"testing" "testing"
"time"
) )
func TestNewBitwardenExport(t *testing.T) { 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{{ export := NewBitwardenExport([]Secret{{
ID: "c381ca2b-0f53-4b05-b209-f2eb11f05e19", ID: "c381ca2b-0f53-4b05-b209-f2eb11f05e19",
Description: "Example", Description: "Example",
@@ -17,8 +13,6 @@ func TestNewBitwardenExport(t *testing.T) {
Notes: "A note", Notes: "A note",
Username: "person@example.com", Username: "person@example.com",
Password: "correct horse battery staple", Password: "correct horse battery staple",
Created: created,
Modified: modified,
}}) }})
if export.Encrypted { 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" { 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) 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) encoded, err := json.Marshal(export)
if err != nil { if err != nil {
t.Fatalf("marshal export: %v", err) t.Fatalf("marshal export: %v", err)
@@ -66,16 +56,20 @@ func TestNewBitwardenExport(t *testing.T) {
if _, ok := document["items"]; !ok { if _, ok := document["items"]; !ok {
t.Fatal("export is missing items field") 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"}}) export := NewBitwardenExport([]Secret{{Description: "No URL"}})
item := export.Items[0] item := export.Items[0]
if item.Login.URIs == nil || len(item.Login.URIs) != 0 { if item.Login.URIs == nil || len(item.Login.URIs) != 0 {
t.Fatalf("expected an empty URI array, got %#v", item.Login.URIs) 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)
}
} }