fix export
This commit is contained in:
@@ -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"`
|
Type int `json:"type"`
|
||||||
RevisionDate string `json:"revisionDate"`
|
Reprompt int `json:"reprompt"`
|
||||||
CreationDate string `json:"creationDate"`
|
Name string `json:"name"`
|
||||||
DeletedDate *string `json:"deletedDate"`
|
Notes string `json:"notes"`
|
||||||
ID string `json:"id"`
|
Favorite bool `json:"favorite"`
|
||||||
OrganizationID *string `json:"organizationId"`
|
Login BitwardenLogin `json:"login"`
|
||||||
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 BitwardenLogin struct {
|
type BitwardenLogin struct {
|
||||||
@@ -51,21 +43,16 @@ func NewBitwardenExport(secrets []Secret) BitwardenExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
items = append(items, BitwardenItem{
|
items = append(items, BitwardenItem{
|
||||||
PasswordHistory: nil,
|
Type: bitwardenLoginItemType,
|
||||||
RevisionDate: bitwardenTime(secret.Modified),
|
Reprompt: 0,
|
||||||
CreationDate: bitwardenTime(secret.Created),
|
Name: secret.Description,
|
||||||
ID: secret.ID,
|
Notes: secret.Notes,
|
||||||
Type: bitwardenLoginItemType,
|
Favorite: false,
|
||||||
Reprompt: 0,
|
|
||||||
Name: secret.Description,
|
|
||||||
Notes: secret.Notes,
|
|
||||||
Favorite: false,
|
|
||||||
Login: BitwardenLogin{
|
Login: BitwardenLogin{
|
||||||
URIs: uris,
|
URIs: uris,
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user