feat: replace Bitwarden CLI with native Go API client (#2)

BREAKING: Removes Node.js/Bitwarden CLI dependency entirely.

## What changed
- New native HTTP client talks directly to Vaultwarden API
- Bitwarden-compatible crypto in pure Go:
  - AES-256-CBC + HMAC-SHA256 decryption
  - PBKDF2-SHA256 and Argon2id key derivation
  - HKDF key stretching (with legacy fallback)
- Auto token refresh (never expires)
- Background vault sync (configurable interval, default 5min)
- Docker image: ~233MB → ~15MB (pure Alpine, no Node.js)

## New env vars
- VAULTWARDEN_EMAIL (replaces CLIENT_ID/CLIENT_SECRET)
- SYNC_INTERVAL (default: 5m)

## Removed
- internal/vaultwarden/client_cli.go (CLI interaction)
- Bitwarden CLI dependency (Node.js + npm package)
- VAULTWARDEN_CLIENT_ID, VAULTWARDEN_CLIENT_SECRET env vars

## Added
- internal/vaultwarden/crypto.go (encryption/decryption)
- internal/vaultwarden/crypto_test.go (11 unit tests)
- internal/vaultwarden/api_client.go (HTTP client)

## Unchanged
- External API surface (GET /secret/:name, POST /refresh, GET /health)
- All security middleware (auth, IP whitelist, rate limiting)
This commit is contained in:
Thijs Herman authored and GitHub committed 2026-02-20 13:28:38 +01:00
1 parent e320f95d9f
commit 9f8ebf2408
14 files changed
+1527 -964

No files matched your search

+8 -9
View File
@@ -1,4 +1,4 @@
// Package handlers provides HTTP request handlers for the API
// Package handlers provides HTTP request handlers for the API.
package handlers
import (
@@ -8,27 +8,27 @@ import (
"github.com/thijsherman/vaultwarden-api/pkg/logger"
)
// Handler contains all HTTP handlers
// Handler contains all HTTP handlers.
type Handler struct {
vaultClient *vaultwarden.Client
}
// NewHandler creates a new handler instance
// NewHandler creates a new handler instance.
func NewHandler(vaultClient *vaultwarden.Client) *Handler {
return &Handler{
vaultClient: vaultClient,
}
}
// HealthCheck handles GET /health
// HealthCheck handles GET /health.
func (h *Handler) HealthCheck(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
"status": "ok",
"service": "vaultwarden-api",
})
}
// GetSecret handles GET /secret/:name
// GetSecret handles GET /secret/:name.
func (h *Handler) GetSecret(c *fiber.Ctx) error {
secretName := c.Params("name")
@@ -48,20 +48,19 @@ func (h *Handler) GetSecret(c *fiber.Ctx) error {
value, err := h.vaultClient.GetSecret(secretName)
if err != nil {
logger.Error.Printf("Failed to fetch secret (requested by IP: %s): %v", c.IP(), err)
logger.Error.Printf("Failed to fetch secret (requested by IP: %s)", c.IP())
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "secret not found",
})
}
// Return the secret value
return c.JSON(fiber.Map{
"name": secretName,
"value": value,
})
}
// RefreshCache handles POST /refresh
// RefreshCache handles POST /refresh.
func (h *Handler) RefreshCache(c *fiber.Ctx) error {
h.vaultClient.ClearCache()