From bb650d9acd78717da2c193e31c06972ffb8433b1 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 2 Aug 2026 17:50:37 +0200 Subject: [PATCH] revert: drop POST /secret write support (back to read-only) --- cmd/api/main.go | 1 - internal/handlers/handlers.go | 62 --------------- internal/vaultwarden/api_client.go | 118 ----------------------------- internal/vaultwarden/client.go | 34 --------- internal/vaultwarden/crypto.go | 75 ++++-------------- 5 files changed, 14 insertions(+), 276 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 00dd131..fb6c7f0 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -122,7 +122,6 @@ func main() { api.Use(auth.Middleware(auth.NewStore(cfg.APIKeys))) api.Get("/secret/:name", h.GetSecret) - api.Post("/secret", h.CreateSecret) api.Post("/refresh", h.RefreshCache) // Graceful shutdown. diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index dc55a83..7f0517a 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -270,65 +270,3 @@ func (h *Handler) RefreshCache(c *fiber.Ctx) error { "message": "cache cleared successfully", }) } - -// CreateSecret handles POST /secret. Body (JSON): -// -// { -// "type": 1, // 1=Login (default), 2=SecureNote -// "name": "...", // required -// "notes": "...", // secure note body (type 2) or login notes -// "username": "...", // login only -// "password": "...", // login only -// "uri": "...", // login only -// "fields": {"k":"v"} // login custom fields (optional) -// } -func (h *Handler) CreateSecret(c *fiber.Ctx) error { - var body struct { - Type int `json:"type"` - Name string `json:"name"` - Notes string `json:"notes"` - Username string `json:"username"` - Password string `json:"password"` - URI string `json:"uri"` - Fields map[string]string `json:"fields"` - } - if err := c.BodyParser(&body); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON body"}) - } - - body.Name = strings.TrimSpace(body.Name) - if body.Name == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"}) - } - if !validators.IsValidSecretName(body.Name) { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid secret name format"}) - } - - 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"}) - } - - 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, - }) - if err != nil { - logger.Error.Printf("Failed to create secret (requested by IP: %s): %v", c.IP(), err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) - } - - return c.Status(fiber.StatusCreated).JSON(fiber.Map{ - "status": "created", - "id": id, - "name": body.Name, - "type": body.Type, - }) -} diff --git a/internal/vaultwarden/api_client.go b/internal/vaultwarden/api_client.go index fac7a8c..498e39c 100644 --- a/internal/vaultwarden/api_client.go +++ b/internal/vaultwarden/api_client.go @@ -1,7 +1,6 @@ package vaultwarden import ( - "bytes" "encoding/json" "fmt" "io" @@ -756,123 +755,6 @@ func (ac *APIClient) doTokenRequest(data url.Values) (*TokenResponse, error) { return &tokenResp, nil } -// CipherInput describes a new vault item to create. -type CipherInput struct { - Type int // 1=Login, 2=SecureNote, 3=Card, 4=Identity - Name string - Notes string - Username string - Password string - URI string - Fields map[string]string // custom fields (login only) -} - -// CreateCipher encrypts and writes a new vault item via the Vaultwarden API. -func (ac *APIClient) CreateCipher(in CipherInput) (string, error) { - if err := ac.EnsureValidToken(); err != nil { - return "", fmt.Errorf("ensure valid token: %w", err) - } - ac.mu.RLock() - token := ac.accessToken - key := ac.symKey - ac.mu.RUnlock() - - encName, err := EncryptStr(in.Name, key) - if err != nil { - return "", fmt.Errorf("encrypt name: %w", err) - } - var encNotes *string - if in.Notes != "" { - n, err := EncryptStr(in.Notes, key) - if err != nil { - return "", fmt.Errorf("encrypt notes: %w", err) - } - encNotes = &n - } - - cipher := map[string]interface{}{ - "type": in.Type, - "name": encName, - } - if encNotes != nil { - cipher["notes"] = *encNotes - } - - switch in.Type { - case CipherTypeLogin: - login := map[string]interface{}{} - if in.Username != "" { - u, err := EncryptStr(in.Username, key); err == nil { - login["username"] = u - } - } - if in.Password != "" { - p, err := EncryptStr(in.Password, key); err == nil { - login["password"] = p - } - } - if in.URI != "" { - u, err := EncryptStr(in.URI, key); err == nil { - login["uris"] = []map[string]interface{}{{"uri": u, "match": nil}} - } - } - cipher["login"] = login - if len(in.Fields) > 0 { - fields := make([]map[string]interface{}, 0, len(in.Fields)) - for k, v := range in.Fields { - fk, err1 := EncryptStr(k, key) - fv, err2 := EncryptStr(v, key) - if err1 == nil && err2 == nil { - fields = append(fields, map[string]interface{}{"name": fk, "value": fv, "type": 0}) - } - } - cipher["fields"] = fields - } - case CipherTypeSecureNote: - // notes already set; nothing else to add. - case CipherTypeCard, CipherTypeIdentity: - // Card/Identity writing is not yet implemented. - return "", fmt.Errorf("cipher type %d write not supported yet", in.Type) - default: - return "", fmt.Errorf("unsupported cipher type: %d", in.Type) - } - - body, err := json.Marshal(cipher) - if err != nil { - return "", fmt.Errorf("marshal cipher: %w", err) - } - - req, err := http.NewRequest("POST", ac.baseURL+"/api/ciphers", bytes.NewReader(body)) - if err != nil { - return "", fmt.Errorf("create request: %w", err) - } - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Content-Type", "application/json") - - resp, err := ac.httpClient.Do(req) - if err != nil { - return "", fmt.Errorf("create cipher request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - b, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("create cipher failed (HTTP %d): %s", resp.StatusCode, string(b)) - } - - var out struct { - ID string `json:"Id"` - Name string `json:"Name"` - } - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return "", fmt.Errorf("decode create response: %w", err) - } - if out.ID == "" { - return "", fmt.Errorf("create cipher returned empty id") - } - return out.ID, nil -} - // fetchProfileKey gets the encrypted symmetric key from the user's profile. // Used when API key login doesn't return the Key in the token response. func (ac *APIClient) fetchProfileKey() (string, error) { diff --git a/internal/vaultwarden/client.go b/internal/vaultwarden/client.go index 3811aa6..6a08c2f 100644 --- a/internal/vaultwarden/client.go +++ b/internal/vaultwarden/client.go @@ -197,40 +197,6 @@ func (c *Client) ClearCache() { } } -// CreateSecretInput is the public input for writing a new secret. -type CreateSecretInput struct { - Type int // 1=Login, 2=SecureNote - Name string - Notes string - Username string - Password string - URI string - Fields map[string]string -} - -// CreateSecret writes a new vault item and refreshes the local cache so it is -// immediately readable via GetSecret. -func (c *Client) CreateSecret(in CreateSecretInput) (string, error) { - if c.api == nil { - 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, - }) - if err != nil { - return "", err - } - // Refresh cache so the new secret is immediately visible. - c.ClearCache() - return id, nil -} - // Stop stops the background sync goroutine. func (c *Client) Stop() { close(c.stopSync) diff --git a/internal/vaultwarden/crypto.go b/internal/vaultwarden/crypto.go index f538089..b4cc04e 100644 --- a/internal/vaultwarden/crypto.go +++ b/internal/vaultwarden/crypto.go @@ -3,11 +3,9 @@ package vaultwarden import ( - "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" - "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" @@ -335,70 +333,25 @@ func DecryptOrgKey(encryptedOrgKey string, privateKey *rsa.PrivateKey) (Symmetri }, nil } -// EncryptStr encrypts a plaintext string using the symmetric key and returns a -// Bitwarden cipher string (EncType 2: AES-256-CBC + HMAC-SHA256). -func EncryptStr(plaintext string, key SymmetricKey) (string, error) { - if len(key.EncKey) != 32 { - return "", fmt.Errorf("invalid enc key length: %d", len(key.EncKey)) +// pkcs7Unpad removes PKCS#7 padding. +func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("empty data") } - if len(key.MacKey) != 32 { - // Legacy mode (no MAC key, type 0). Unlikely for modern vaults. - return encryptAesCbc256_B64(plaintext, key.EncKey) + if len(data)%blockSize != 0 { + return nil, errors.New("data not block-aligned") } - iv := make([]byte, aes.BlockSize) - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", fmt.Errorf("generate iv: %w", err) + padLen := int(data[len(data)-1]) + if padLen == 0 || padLen > blockSize { + return nil, fmt.Errorf("invalid padding length: %d", padLen) } - block, err := aes.NewCipher(key.EncKey) - if err != nil { - return "", fmt.Errorf("aes cipher: %w", err) + for i := len(data) - padLen; i < len(data); i++ { + if data[i] != byte(padLen) { + return nil, errors.New("invalid PKCS7 padding") + } } - // PKCS7 pad. - plain := []byte(plaintext) - pad := aes.BlockSize - len(plain)%aes.BlockSize - padded := append(plain, bytes.Repeat([]byte{byte(pad)}, pad)...) - - ct := make([]byte, len(padded)) - mode := cipher.NewCBCEncrypter(block, iv) - mode.CryptBlocks(ct, padded) - - // MAC over iv || ct. - mac := hmac.New(sha256.New, key.MacKey) - mac.Write(iv) - mac.Write(ct) - expectedMAC := mac.Sum(nil) - - cs := fmt.Sprintf("%d.%s|%s|%s", - EncTypeAesCbc256_HmacSha256_B64, - base64.StdEncoding.EncodeToString(iv), - base64.StdEncoding.EncodeToString(ct), - base64.StdEncoding.EncodeToString(expectedMAC), - ) - return cs, nil -} - -// encryptAesCbc256_B64 encrypts without a MAC (legacy EncType 0). -func encryptAesCbc256_B64(plaintext string, encKey []byte) (string, error) { - iv := make([]byte, aes.BlockSize) - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", fmt.Errorf("generate iv: %w", err) - } - block, err := aes.NewCipher(encKey) - if err != nil { - return "", fmt.Errorf("aes cipher: %w", err) - } - plain := []byte(plaintext) - pad := aes.BlockSize - len(plain)%aes.BlockSize - padded := append(plain, bytes.Repeat([]byte{byte(pad)}, pad)...) - ct := make([]byte, len(padded)) - mode := cipher.NewCBCEncrypter(block, iv) - mode.CryptBlocks(ct, padded) - return fmt.Sprintf("%d.%s|%s", - EncTypeAesCbc256_B64, - base64.StdEncoding.EncodeToString(iv), - base64.StdEncoding.EncodeToString(ct), - ), nil + return data[:len(data)-padLen], nil }