feat: write Card (3) and Identity (4) ciphers
CI / test (push) Has been cancelled
CI / docker (push) Has been cancelled

Add card/identity sub-objects (camelCase serde per Vaultwarden API).
brand/expMonth/expYear sent plaintext; other fields encrypted.
Verified end-to-end: create + read decrypt correctly.
This commit is contained in:
Hermes committed 2026-08-02 19:12:57 +02:00
1 parent b48cb16746
commit 0ede8f145d
3 files changed
+198 -17

No files matched your search

+57 -9
View File
@@ -291,6 +291,31 @@ func (h *Handler) CreateSecret(c *fiber.Ctx) error {
Password string `json:"password"` Password string `json:"password"`
URI string `json:"uri"` URI string `json:"uri"`
Fields map[string]string `json:"fields"` Fields map[string]string `json:"fields"`
// Card (type 3)
CardholderName string `json:"cardholderName"`
Brand string `json:"brand"`
Number string `json:"number"`
ExpMonth string `json:"expMonth"`
ExpYear string `json:"expYear"`
Code string `json:"code"`
// Identity (type 4)
Title string `json:"title"`
FirstName string `json:"firstName"`
MiddleName string `json:"middleName"`
LastName string `json:"lastName"`
Address1 string `json:"address1"`
Address2 string `json:"address2"`
Address3 string `json:"address3"`
City string `json:"city"`
State string `json:"state"`
PostalCode string `json:"postalCode"`
Country string `json:"country"`
Company string `json:"company"`
Email string `json:"email"`
Phone string `json:"phone"`
SSN string `json:"ssn"`
Passport string `json:"passport"`
License string `json:"license"`
} }
if err := c.BodyParser(&body); err != nil { if err := c.BodyParser(&body); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON body"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON body"})
@@ -307,18 +332,41 @@ func (h *Handler) CreateSecret(c *fiber.Ctx) error {
if body.Type == 0 { if body.Type == 0 {
body.Type = 1 // default to Login body.Type = 1 // default to Login
} }
if body.Type != 1 && body.Type != 2 { if body.Type < 1 || body.Type > 4 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "only Login (1) and SecureNote (2) are supported for writing"}) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "supported types: 1=Login, 2=SecureNote, 3=Card, 4=Identity"})
} }
id, err := h.vaultClient.CreateSecret(vaultwarden.CreateSecretInput{ id, err := h.vaultClient.CreateSecret(vaultwarden.CreateSecretInput{
Type: body.Type, Type: body.Type,
Name: body.Name, Name: body.Name,
Notes: body.Notes, Notes: body.Notes,
Username: body.Username, Username: body.Username,
Password: body.Password, Password: body.Password,
URI: body.URI, URI: body.URI,
Fields: body.Fields, Fields: body.Fields,
CardholderName: body.CardholderName,
Brand: body.Brand,
Number: body.Number,
ExpMonth: body.ExpMonth,
ExpYear: body.ExpYear,
Code: body.Code,
Title: body.Title,
FirstName: body.FirstName,
MiddleName: body.MiddleName,
LastName: body.LastName,
Address1: body.Address1,
Address2: body.Address2,
Address3: body.Address3,
City: body.City,
State: body.State,
PostalCode: body.PostalCode,
Country: body.Country,
Company: body.Company,
Email: body.Email,
Phone: body.Phone,
SSN: body.SSN,
Passport: body.Passport,
License: body.License,
}) })
if err != nil { if err != nil {
logger.Error.Printf("Failed to create secret (requested by IP: %s): %v", c.IP(), err) logger.Error.Printf("Failed to create secret (requested by IP: %s): %v", c.IP(), err)
+85
View File
@@ -765,6 +765,32 @@ type CipherInput struct {
Password string Password string
URI string URI string
Fields map[string]string // custom fields (login only) Fields map[string]string // custom fields (login only)
// Card (type 3)
CardholderName string
Brand string // plaintext enum string, NOT encrypted
Number string
ExpMonth string // plaintext "01".."12"
ExpYear string // plaintext "2026"
Code string // CVV
// Identity (type 4)
Title string
FirstName string
MiddleName string
LastName string
Address1 string
Address2 string
Address3 string
City string
State string
PostalCode string
Country string
Company string
Email string
Phone string
SSN string
Passport string
License string
// Identity also reuses Username field above.
} }
// CreateCipher encrypts and writes a new vault item via the Vaultwarden API. // CreateCipher encrypts and writes a new vault item via the Vaultwarden API.
@@ -844,6 +870,65 @@ func (ac *APIClient) CreateCipher(in CipherInput) (string, error) {
cipher["secureNote"] = map[string]interface{}{"notes": encNotes} cipher["secureNote"] = map[string]interface{}{"notes": encNotes}
} }
} }
} else if in.Type == CipherTypeCard {
// Vaultwarden expects type=3 data in the `card` sub-object (camelCase serde).
// brand/expMonth/expYear are plaintext enum strings; others are encrypted.
card := map[string]interface{}{}
if in.CardholderName != "" {
if s, e := EncryptStr(in.CardholderName, key); e == nil {
card["cardholderName"] = s
}
}
if in.Brand != "" {
card["brand"] = in.Brand
}
if in.Number != "" {
if s, e := EncryptStr(in.Number, key); e == nil {
card["number"] = s
}
}
if in.ExpMonth != "" {
card["expMonth"] = in.ExpMonth
}
if in.ExpYear != "" {
card["expYear"] = in.ExpYear
}
if in.Code != "" {
if s, e := EncryptStr(in.Code, key); e == nil {
card["code"] = s
}
}
cipher["card"] = card
} else if in.Type == CipherTypeIdentity {
// Vaultwarden expects type=4 data in the `identity` sub-object (camelCase serde).
idn := map[string]interface{}{}
enc := func(field, val string) {
if val == "" {
return
}
if s, e := EncryptStr(val, key); e == nil {
idn[field] = s
}
}
enc("title", in.Title)
enc("firstName", in.FirstName)
enc("middleName", in.MiddleName)
enc("lastName", in.LastName)
enc("address1", in.Address1)
enc("address2", in.Address2)
enc("address3", in.Address3)
enc("city", in.City)
enc("state", in.State)
enc("postalCode", in.PostalCode)
enc("country", in.Country)
enc("company", in.Company)
enc("email", in.Email)
enc("phone", in.Phone)
enc("username", in.Username)
enc("ssn", in.SSN)
enc("passportNumber", in.Passport)
enc("licenseNumber", in.License)
cipher["identity"] = idn
} else { } else {
return "", fmt.Errorf("cipher type %d write not supported yet", in.Type) return "", fmt.Errorf("cipher type %d write not supported yet", in.Type)
} }
+56 -8
View File
@@ -199,13 +199,38 @@ func (c *Client) ClearCache() {
// CreateSecretInput is the public input for writing a new secret. // CreateSecretInput is the public input for writing a new secret.
type CreateSecretInput struct { type CreateSecretInput struct {
Type int // 1=Login, 2=SecureNote Type int // 1=Login, 2=SecureNote, 3=Card, 4=Identity
Name string Name string
Notes string Notes string
Username string Username string
Password string Password string
URI string URI string
Fields map[string]string Fields map[string]string
// Card (type 3)
CardholderName string
Brand string
Number string
ExpMonth string
ExpYear string
Code string
// Identity (type 4)
Title string
FirstName string
MiddleName string
LastName string
Address1 string
Address2 string
Address3 string
City string
State string
PostalCode string
Country string
Company string
Email string
Phone string
SSN string
Passport string
License string
} }
// CreateSecret writes a new vault item and refreshes the local cache so it is // CreateSecret writes a new vault item and refreshes the local cache so it is
@@ -215,13 +240,36 @@ func (c *Client) CreateSecret(in CreateSecretInput) (string, error) {
return "", fmt.Errorf("vault API client not configured") return "", fmt.Errorf("vault API client not configured")
} }
id, err := c.api.CreateCipher(CipherInput{ id, err := c.api.CreateCipher(CipherInput{
Type: in.Type, Type: in.Type,
Name: in.Name, Name: in.Name,
Notes: in.Notes, Notes: in.Notes,
Username: in.Username, Username: in.Username,
Password: in.Password, Password: in.Password,
URI: in.URI, URI: in.URI,
Fields: in.Fields, Fields: in.Fields,
CardholderName: in.CardholderName,
Brand: in.Brand,
Number: in.Number,
ExpMonth: in.ExpMonth,
ExpYear: in.ExpYear,
Code: in.Code,
Title: in.Title,
FirstName: in.FirstName,
MiddleName: in.MiddleName,
LastName: in.LastName,
Address1: in.Address1,
Address2: in.Address2,
Address3: in.Address3,
City: in.City,
State: in.State,
PostalCode: in.PostalCode,
Country: in.Country,
Company: in.Company,
Email: in.Email,
Phone: in.Phone,
SSN: in.SSN,
Passport: in.Passport,
License: in.License,
}) })
if err != nil { if err != nil {
return "", err return "", err