diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index dc55a83..2073ce3 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -291,6 +291,31 @@ func (h *Handler) CreateSecret(c *fiber.Ctx) error { Password string `json:"password"` URI string `json:"uri"` 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 { 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 { body.Type = 1 // default to Login } - if body.Type != 1 && body.Type != 2 { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "only Login (1) and SecureNote (2) are supported for writing"}) + if body.Type < 1 || body.Type > 4 { + 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{ - Type: body.Type, - Name: body.Name, - Notes: body.Notes, - Username: body.Username, - Password: body.Password, - URI: body.URI, - Fields: body.Fields, + Type: body.Type, + Name: body.Name, + Notes: body.Notes, + Username: body.Username, + Password: body.Password, + URI: body.URI, + 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 { logger.Error.Printf("Failed to create secret (requested by IP: %s): %v", c.IP(), err) diff --git a/internal/vaultwarden/api_client.go b/internal/vaultwarden/api_client.go index db61425..3ef929c 100644 --- a/internal/vaultwarden/api_client.go +++ b/internal/vaultwarden/api_client.go @@ -765,6 +765,32 @@ type CipherInput struct { Password string URI string 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. @@ -844,6 +870,65 @@ func (ac *APIClient) CreateCipher(in CipherInput) (string, error) { 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 { return "", fmt.Errorf("cipher type %d write not supported yet", in.Type) } diff --git a/internal/vaultwarden/client.go b/internal/vaultwarden/client.go index 3811aa6..0627ba3 100644 --- a/internal/vaultwarden/client.go +++ b/internal/vaultwarden/client.go @@ -199,13 +199,38 @@ func (c *Client) ClearCache() { // CreateSecretInput is the public input for writing a new secret. type CreateSecretInput struct { - Type int // 1=Login, 2=SecureNote + Type int // 1=Login, 2=SecureNote, 3=Card, 4=Identity Name string Notes string Username string Password string URI 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 @@ -215,13 +240,36 @@ func (c *Client) CreateSecret(in CreateSecretInput) (string, error) { return "", fmt.Errorf("vault API client not configured") } id, err := c.api.CreateCipher(CipherInput{ - Type: in.Type, - Name: in.Name, - Notes: in.Notes, - Username: in.Username, - Password: in.Password, - URI: in.URI, - Fields: in.Fields, + Type: in.Type, + Name: in.Name, + Notes: in.Notes, + Username: in.Username, + Password: in.Password, + URI: in.URI, + 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 { return "", err