feat: write Card (3) and Identity (4) ciphers
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:
1 parent
b48cb16746
commit
0ede8f145d
3 files changed
+198
-17
No files matched your search
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user