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:
1 parent
e320f95d9f
commit
9f8ebf2408
14 files changed
+1527
-964
No files matched your search
@@ -1,114 +1,40 @@
|
||||
package vaultwarden
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thijsherman/vaultwarden-api/pkg/logger"
|
||||
)
|
||||
|
||||
var bitwardenInitMutex sync.Mutex
|
||||
// InitializeClient creates and initializes a fully authenticated vault client.
|
||||
// clientID and clientSecret are optional — if provided, API key login is used (bypasses 2FA).
|
||||
func InitializeClient(serverURL, email, password, clientID, clientSecret string, cacheTTL, syncInterval time.Duration) (*Client, error) {
|
||||
logger.Info.Println("Initializing Vaultwarden native API client...")
|
||||
|
||||
type BitwardenStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
api := NewAPIClient(serverURL, email, password, clientID, clientSecret)
|
||||
client := NewClient(api, cacheTTL, syncInterval)
|
||||
|
||||
func InitializeBitwardenCLI(serverURL, clientID, clientSecret, password string) (string, error) {
|
||||
bitwardenInitMutex.Lock()
|
||||
defer bitwardenInitMutex.Unlock()
|
||||
// Authenticate and perform initial sync with retry.
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
logger.Info.Println("Initializing Bitwarden CLI...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
cmd := exec.CommandContext(ctx, "bw", "config", "server", serverURL)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
cancel()
|
||||
return "", fmt.Errorf("config server failed: %w - %s", err, output)
|
||||
}
|
||||
cancel()
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, "bw", "status")
|
||||
statusOutput, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
logger.Warn.Printf("Failed to check status: %v", err)
|
||||
}
|
||||
|
||||
var status BitwardenStatus
|
||||
isLoggedIn := false
|
||||
if err := json.Unmarshal(statusOutput, &status); err == nil {
|
||||
isLoggedIn = status.Status == "unlocked" || status.Status == "locked"
|
||||
}
|
||||
|
||||
if !isLoggedIn {
|
||||
logger.Info.Println("Logging in to Bitwarden...")
|
||||
maxRetries := 3
|
||||
var loginErr error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
backoff := time.Duration(attempt*attempt) * 5 * time.Second
|
||||
logger.Info.Printf("Retry attempt %d/%d after %v...", attempt, maxRetries, backoff)
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||
cmd = exec.CommandContext(ctx, "bw", "login", "--apikey")
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("BW_CLIENTID=%s", clientID),
|
||||
fmt.Sprintf("BW_CLIENTSECRET=%s", clientSecret),
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
cancel()
|
||||
|
||||
outputStr := strings.TrimSpace(string(output))
|
||||
if err == nil || strings.Contains(outputStr, "You are logged in!") {
|
||||
logger.Info.Println("Login successful")
|
||||
loginErr = nil
|
||||
break
|
||||
}
|
||||
|
||||
if strings.Contains(outputStr, "Rate limit") {
|
||||
logger.Warn.Printf("Rate limited (attempt %d/%d)", attempt, maxRetries)
|
||||
loginErr = fmt.Errorf("rate limited: %s", outputStr)
|
||||
} else {
|
||||
logger.Warn.Printf("Login failed (attempt %d/%d): %v - %s", attempt, maxRetries, err, outputStr)
|
||||
loginErr = fmt.Errorf("login failed: %w - %s", err, outputStr)
|
||||
}
|
||||
|
||||
if attempt < maxRetries {
|
||||
continue
|
||||
}
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
backoff := time.Duration(attempt*attempt) * 5 * time.Second
|
||||
logger.Info.Printf("Retry attempt %d/%d after %v...", attempt, maxRetries, backoff)
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
if loginErr != nil {
|
||||
return "", loginErr
|
||||
if err := client.Initialize(); err != nil {
|
||||
logger.Warn.Printf("Initialization failed (attempt %d/%d): %v", attempt, maxRetries, err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
logger.Info.Println("Already logged in, skipping login")
|
||||
|
||||
logger.Info.Println("Vaultwarden client initialized successfully")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, "bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw")
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password))
|
||||
sessionToken, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unlock failed: %w - %s", err, sessionToken)
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(string(sessionToken))
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("received empty session token")
|
||||
}
|
||||
|
||||
logger.Info.Println("Bitwarden CLI initialized successfully")
|
||||
return token, nil
|
||||
return nil, fmt.Errorf("failed to initialize after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
Reference in new issue
Block a user