Files
zeekay baebcfa594 fix: add GOWORK/CGO env to all CI workflows and fix go fmt
- Add GOWORK=off and CGO_ENABLED=0 to lint.yml, unit-test.yml, ci.yml, build.yml
- Remove -race flag from ci.yml (incompatible with CGO_ENABLED=0)
- Fix go fmt formatting in 22 Go files
2026-01-28 10:21:23 -08:00

1295 lines
35 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package kms
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// Server provides the HTTP API for KMS operations.
// API is compatible with the kms-go SDK client at github.com/luxfi/kms-go
type Server struct {
kms *KMS
mpc *MPCManager
addr string
server *http.Server
config *ServerConfig
}
// ServerConfig holds server configuration.
type ServerConfig struct {
Addr string
ReadTimeout time.Duration
WriteTimeout time.Duration
MaxHeaderBytes int
CORSOrigins []string
APIKey string // Simple API key authentication
EnableMPC bool
EnableSecrets bool
}
// DefaultServerConfig returns default server configuration.
func DefaultServerConfig() *ServerConfig {
return &ServerConfig{
Addr: ":8200",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
CORSOrigins: []string{"*"},
EnableMPC: true,
EnableSecrets: true,
}
}
// NewServer creates a new KMS HTTP server.
func NewServer(kms *KMS, cfg *ServerConfig) *Server {
if cfg == nil {
cfg = DefaultServerConfig()
}
s := &Server{
kms: kms,
mpc: NewMPCManager(kms),
addr: cfg.Addr,
config: cfg,
}
mux := http.NewServeMux()
// Health check
mux.HandleFunc("/health", s.handleHealth)
mux.HandleFunc("/v1/health", s.handleHealth)
// KMS Key management - compatible with kms-go SDK
mux.HandleFunc("/v1/kms/keys", s.handleKmsKeys)
mux.HandleFunc("/v1/kms/keys/", s.handleKmsKey)
// Legacy endpoints (for backwards compatibility)
mux.HandleFunc("/v1/keys", s.handleKmsKeys)
mux.HandleFunc("/v1/keys/", s.handleKmsKey)
// Legacy encryption/signing endpoints
mux.HandleFunc("/v1/encrypt", s.handleLegacyEncrypt)
mux.HandleFunc("/v1/decrypt", s.handleLegacyDecrypt)
mux.HandleFunc("/v1/sign", s.handleLegacySign)
mux.HandleFunc("/v1/verify", s.handleLegacyVerify)
// Secrets - v3 API compatible with kms-go SDK
if cfg.EnableSecrets {
mux.HandleFunc("/v3/secrets/raw", s.handleSecretsV3)
mux.HandleFunc("/v3/secrets/raw/", s.handleSecretV3)
mux.HandleFunc("/v3/secrets/batch/raw", s.handleSecretsBatchV3)
// Legacy v1 endpoints
mux.HandleFunc("/v1/secrets", s.handleSecretsV3)
mux.HandleFunc("/v1/secrets/", s.handleSecretV3)
}
// MPC endpoints (if enabled)
if cfg.EnableMPC {
mux.HandleFunc("/v1/mpc/nodes", s.handleMPCNodes)
mux.HandleFunc("/v1/mpc/nodes/", s.handleMPCNode)
mux.HandleFunc("/v1/mpc/wallets", s.handleMPCWallets)
mux.HandleFunc("/v1/mpc/wallets/", s.handleMPCWallet)
mux.HandleFunc("/v1/mpc/sign", s.handleMPCSign)
mux.HandleFunc("/v1/mpc/signing/", s.handleMPCSigning)
}
s.server = &http.Server{
Addr: cfg.Addr,
Handler: s.middleware(mux),
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
MaxHeaderBytes: cfg.MaxHeaderBytes,
}
return s
}
// Start starts the HTTP server.
func (s *Server) Start() error {
return s.server.ListenAndServe()
}
// Stop gracefully shuts down the server.
func (s *Server) Stop(ctx context.Context) error {
return s.server.Shutdown(ctx)
}
// middleware adds common middleware to all requests.
func (s *Server) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CORS
origin := r.Header.Get("Origin")
if origin != "" {
for _, allowed := range s.config.CORSOrigins {
if allowed == "*" || allowed == origin {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
break
}
}
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// API Key authentication (if configured)
if s.config.APIKey != "" {
apiKey := r.Header.Get("X-API-Key")
if apiKey == "" {
apiKey = r.Header.Get("Authorization")
if strings.HasPrefix(apiKey, "Bearer ") {
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
}
}
if apiKey != s.config.APIKey {
s.writeError(w, http.StatusUnauthorized, "invalid API key")
return
}
}
// Content-Type
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
// Response types matching kms-go SDK expectations
type errorResponse struct {
Error string `json:"error"`
StatusCode int `json:"statusCode"`
Message string `json:"message,omitempty"`
}
func (s *Server) writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func (s *Server) writeError(w http.ResponseWriter, status int, message string) {
s.writeJSON(w, status, errorResponse{
Error: message,
StatusCode: status,
Message: message,
})
}
// Health check handler
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
}
// KmsKey matches kms-go SDK KmsKey struct
type KmsKey struct {
ID string `json:"id"`
Description string `json:"description"`
IsDisabled bool `json:"isDisabled"`
OrgID string `json:"orgId"`
Name string `json:"name"`
ProjectID string `json:"projectId"`
KeyUsage string `json:"keyUsage"` // "sign-verify" or "encrypt-decrypt"
Version int `json:"version"`
EncryptionAlgorithm string `json:"encryptionAlgorithm"` // "rsa-4096", "ecc-nist-p256", "aes-256-gcm", "aes-128-gcm"
}
// Convert internal Key to SDK-compatible KmsKey
func keyToKmsKey(key *Key) KmsKey {
keyUsage := "encrypt-decrypt"
if key.Usage == KeyUsageSignVerify {
keyUsage = "sign-verify"
}
encAlg := string(key.Type)
// Map internal types to SDK-expected format
switch key.Type {
case KeyTypeAES256:
encAlg = "aes-256-gcm"
case KeyTypeRSA3072, KeyTypeRSA4096:
encAlg = "rsa-4096"
case KeyTypeECDSAP256, KeyTypeECDSAP384:
encAlg = "ecc-nist-p256"
case KeyTypeEdDSA:
encAlg = "ed25519"
}
return KmsKey{
ID: key.ID,
Description: key.Description,
IsDisabled: key.Status != KeyStatusActive,
OrgID: key.OrgID,
Name: key.Name,
ProjectID: key.ProjectID,
KeyUsage: keyUsage,
Version: key.Version,
EncryptionAlgorithm: encAlg,
}
}
// KMS Key management handlers - compatible with kms-go SDK
func (s *Server) handleKmsKeys(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
switch r.Method {
case "GET":
keys, err := s.kms.ListKeys(ctx, "")
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Convert to SDK format
kmsKeys := make([]KmsKey, len(keys))
for i, key := range keys {
kmsKeys[i] = keyToKmsKey(key)
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"keys": kmsKeys})
case "POST":
// KmsCreateKeyV1Request format
var req struct {
KeyUsage string `json:"keyUsage"` // "sign-verify" or "encrypt-decrypt"
Description string `json:"description"`
Name string `json:"name"`
EncryptionAlgorithm string `json:"encryptionAlgorithm"` // "rsa-4096", "ecc-nist-p256", "aes-256-gcm", "aes-128-gcm"
ProjectID string `json:"projectId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
// Map SDK algorithm names to internal types
var keyType KeyType
switch req.EncryptionAlgorithm {
case "aes-256-gcm":
keyType = KeyTypeAES256
case "aes-128-gcm":
keyType = KeyTypeAES256 // Use AES-256 for both
case "rsa-4096":
keyType = KeyTypeRSA4096
case "ecc-nist-p256":
keyType = KeyTypeECDSAP256
default:
keyType = KeyTypeAES256
}
var keyUsage KeyUsage
if req.KeyUsage == "sign-verify" {
keyUsage = KeyUsageSignVerify
} else {
keyUsage = KeyUsageEncryptDecrypt
}
opts := &KeyOptions{
Description: req.Description,
ProjectID: req.ProjectID,
}
key, err := s.kms.GenerateKey(ctx, req.Name, keyType, keyUsage, opts)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Return in KmsCreateKeyV1Response format
s.writeJSON(w, http.StatusCreated, map[string]interface{}{
"key": keyToKmsKey(key),
})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleKmsKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Parse path: /v1/kms/keys/{keyId} or /v1/kms/keys/{keyId}/{action}
// or /v1/kms/keys/name/{keyName}/project/{projectId}
path := r.URL.Path
var keyID string
// Handle different path patterns
if strings.Contains(path, "/v1/kms/keys/") {
path = strings.TrimPrefix(path, "/v1/kms/keys/")
} else if strings.Contains(path, "/v1/keys/") {
path = strings.TrimPrefix(path, "/v1/keys/")
}
parts := strings.Split(path, "/")
// Check for /name/{keyName}/project/{projectId} pattern
if len(parts) >= 4 && parts[0] == "name" {
keyName := parts[1]
projectID := ""
if len(parts) >= 4 && parts[2] == "project" {
projectID = parts[3]
}
s.handleGetKeyByName(w, r, keyName, projectID)
return
}
keyID = parts[0]
action := ""
if len(parts) > 1 {
action = parts[1]
}
// Route to appropriate handler based on action
switch action {
case "encrypt":
s.handleKeyEncrypt(w, r, keyID)
case "decrypt":
s.handleKeyDecrypt(w, r, keyID)
case "sign":
s.handleKeySign(w, r, keyID)
case "verify":
s.handleKeyVerify(w, r, keyID)
case "public-key":
s.handleKeyPublicKey(w, r, keyID)
case "signing-algorithms":
s.handleKeySigningAlgorithms(w, r, keyID)
case "":
// Direct key operations
switch r.Method {
case "GET":
key, err := s.kms.GetKey(ctx, keyID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "key not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"key": keyToKmsKey(key),
})
case "DELETE":
key, err := s.kms.GetKey(ctx, keyID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "key not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
kmsKey := keyToKmsKey(key)
if err := s.kms.DeleteKey(ctx, keyID); err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// KmsDeleteKeyV1Response format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"key": kmsKey,
})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
default:
s.writeError(w, http.StatusNotFound, "unknown action")
}
}
func (s *Server) handleGetKeyByName(w http.ResponseWriter, r *http.Request, keyName, projectID string) {
if r.Method != "GET" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// List keys and find by name
keys, err := s.kms.ListKeys(ctx, "")
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
for _, key := range keys {
if key.Name == keyName && (projectID == "" || key.ProjectID == projectID) {
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"key": keyToKmsKey(key),
})
return
}
}
s.writeError(w, http.StatusNotFound, "key not found")
}
func (s *Server) handleKeyEncrypt(w http.ResponseWriter, r *http.Request, keyID string) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// KmsEncryptDataV1Request format
var req struct {
Plaintext string `json:"plaintext"` // Base64 encoded
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
plaintext, err := DecodeBase64(req.Plaintext)
if err != nil {
// Try as raw string
plaintext = []byte(req.Plaintext)
}
ciphertext, err := s.kms.Encrypt(ctx, keyID, plaintext)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// KmsEncryptDataV1Response format
s.writeJSON(w, http.StatusOK, map[string]string{
"ciphertext": EncodeBase64(ciphertext),
})
}
func (s *Server) handleKeyDecrypt(w http.ResponseWriter, r *http.Request, keyID string) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// KmsDecryptDataV1Request format
var req struct {
Ciphertext string `json:"ciphertext"` // Base64 encoded
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
ciphertext, err := DecodeBase64(req.Ciphertext)
if err != nil {
s.writeError(w, http.StatusBadRequest, "invalid ciphertext encoding")
return
}
plaintext, err := s.kms.Decrypt(ctx, ciphertext)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// KmsDecryptDataV1Response format
s.writeJSON(w, http.StatusOK, map[string]string{
"plaintext": EncodeBase64(plaintext),
})
}
func (s *Server) handleKeySign(w http.ResponseWriter, r *http.Request, keyID string) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// KmsSignDataV1Request format
var req struct {
Data string `json:"data"` // Base64 encoded
SigningAlgorithm string `json:"signingAlgorithm"` // e.g., "RSASSA_PKCS1_V1_5_SHA_256"
IsDigest bool `json:"isDigest"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
data, err := DecodeBase64(req.Data)
if err != nil {
s.writeError(w, http.StatusBadRequest, "invalid data encoding")
return
}
signature, err := s.kms.Sign(ctx, keyID, data)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// KmsSignDataV1Response format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"signature": EncodeBase64(signature),
"keyId": keyID,
"signingAlgorithm": req.SigningAlgorithm,
})
}
func (s *Server) handleKeyVerify(w http.ResponseWriter, r *http.Request, keyID string) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// KmsVerifyDataV1Request format
var req struct {
Data string `json:"data"` // Base64 encoded
Signature string `json:"signature"` // Base64 encoded
SigningAlgorithm string `json:"signingAlgorithm"`
IsDigest bool `json:"isDigest"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
data, err := DecodeBase64(req.Data)
if err != nil {
s.writeError(w, http.StatusBadRequest, "invalid data encoding")
return
}
signature, err := DecodeBase64(req.Signature)
if err != nil {
s.writeError(w, http.StatusBadRequest, "invalid signature encoding")
return
}
valid, err := s.kms.Verify(ctx, keyID, data, signature)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// KmsVerifyDataV1Response format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"signatureValid": valid,
"keyId": keyID,
"signingAlgorithm": req.SigningAlgorithm,
})
}
func (s *Server) handleKeyPublicKey(w http.ResponseWriter, r *http.Request, keyID string) {
if r.Method != "GET" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// Use GetPublicKey which properly retrieves from KeyMaterial
publicKey, err := s.kms.GetPublicKey(ctx, keyID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "key not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// KmsGetPublicKeyV1Response format
s.writeJSON(w, http.StatusOK, map[string]string{
"publicKey": EncodeBase64(publicKey),
})
}
func (s *Server) handleKeySigningAlgorithms(w http.ResponseWriter, r *http.Request, keyID string) {
if r.Method != "GET" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
key, err := s.kms.GetKey(ctx, keyID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "key not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Return algorithms based on key type
var algorithms []string
switch key.Type {
case KeyTypeRSA3072, KeyTypeRSA4096:
algorithms = []string{
"RSASSA_PKCS1_V1_5_SHA_256",
"RSASSA_PKCS1_V1_5_SHA_384",
"RSASSA_PKCS1_V1_5_SHA_512",
"RSASSA_PSS_SHA_256",
"RSASSA_PSS_SHA_384",
"RSASSA_PSS_SHA_512",
}
case KeyTypeECDSAP256:
algorithms = []string{"ECDSA_SHA_256"}
case KeyTypeECDSAP384:
algorithms = []string{"ECDSA_SHA_384"}
case KeyTypeEdDSA:
algorithms = []string{"EDDSA"}
default:
algorithms = []string{}
}
// KmsListSigningAlgorithmsV1Response format
s.writeJSON(w, http.StatusOK, map[string][]string{
"signingAlgorithms": algorithms,
})
}
// Legacy encryption handlers (backwards compatibility)
func (s *Server) handleLegacyEncrypt(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
KeyID string `json:"keyId"`
Plaintext string `json:"plaintext"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
s.handleKeyEncrypt(w, r, req.KeyID)
}
func (s *Server) handleLegacyDecrypt(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
KeyID string `json:"keyId"`
Ciphertext string `json:"ciphertext"`
}
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
// Re-create request with just ciphertext for the handler
ctx := r.Context()
ciphertext, err := DecodeBase64(req.Ciphertext)
if err != nil {
s.writeError(w, http.StatusBadRequest, "invalid ciphertext encoding")
return
}
plaintext, err := s.kms.Decrypt(ctx, ciphertext)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]string{
"plaintext": EncodeBase64(plaintext),
})
}
func (s *Server) handleLegacySign(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
KeyID string `json:"keyId"`
Data string `json:"data"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
s.handleKeySign(w, r, req.KeyID)
}
func (s *Server) handleLegacyVerify(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
KeyID string `json:"keyId"`
Data string `json:"data"`
Signature string `json:"signature"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
s.handleKeyVerify(w, r, req.KeyID)
}
// Secret response matching kms-go SDK Secret model
type SecretResponse struct {
ID string `json:"id"`
SecretKey string `json:"secretKey"`
SecretValue string `json:"secretValue,omitempty"`
Version int `json:"version"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
}
func secretToResponse(sec *Secret, includeValue bool, decryptedValue []byte) SecretResponse {
resp := SecretResponse{
ID: sec.ID,
SecretKey: sec.Name,
Version: sec.Version,
Type: "shared", // Default type
Environment: sec.Environment,
SecretPath: sec.Path,
}
if includeValue && decryptedValue != nil {
resp.SecretValue = string(decryptedValue)
}
return resp
}
// Secrets V3 handlers - compatible with kms-go SDK
func (s *Server) handleSecretsV3(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
switch r.Method {
case "GET":
// ListSecretsV3RawRequest query params
projectID := r.URL.Query().Get("workspaceId")
if projectID == "" {
projectID = r.URL.Query().Get("workspaceSlug")
}
environment := r.URL.Query().Get("environment")
secretPath := r.URL.Query().Get("secretPath")
if secretPath == "" {
secretPath = "/"
}
secrets, err := s.kms.ListSecrets(ctx, environment, secretPath)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Convert to SDK response format
secretResponses := make([]SecretResponse, len(secrets))
for i, sec := range secrets {
// Filter by project if specified
if projectID != "" && sec.ProjectID != projectID {
continue
}
secretResponses[i] = secretToResponse(sec, false, nil)
}
// ListSecretsV3RawResponse format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"secrets": secretResponses,
"imports": []interface{}{},
})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleSecretV3(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Parse secret key from path
path := r.URL.Path
var secretKey string
if strings.Contains(path, "/v3/secrets/raw/") {
secretKey = strings.TrimPrefix(path, "/v3/secrets/raw/")
} else if strings.Contains(path, "/v1/secrets/") {
secretKey = strings.TrimPrefix(path, "/v1/secrets/")
}
// Handle /value suffix for getting decrypted value
getValue := false
if strings.HasSuffix(secretKey, "/value") {
secretKey = strings.TrimSuffix(secretKey, "/value")
getValue = true
}
switch r.Method {
case "GET":
// RetrieveSecretV3RawRequest query params
projectID := r.URL.Query().Get("workspaceId")
if projectID == "" {
projectID = r.URL.Query().Get("workspaceSlug")
}
environment := r.URL.Query().Get("environment")
secretPath := r.URL.Query().Get("secretPath")
// Find secret by name
secrets, err := s.kms.ListSecrets(ctx, environment, secretPath)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
for _, sec := range secrets {
if sec.Name == secretKey && (projectID == "" || sec.ProjectID == projectID) {
var value []byte
if getValue {
value, err = s.kms.GetSecretValue(ctx, sec.ID)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
// RetrieveSecretV3RawResponse format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"secret": secretToResponse(sec, getValue, value),
})
return
}
}
s.writeError(w, http.StatusNotFound, "secret not found")
case "POST":
// CreateSecretV3RawRequest format
var req struct {
ProjectID string `json:"workspaceId"`
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
Type string `json:"type"`
SecretComment string `json:"secretComment"`
SkipMultiLineEncoding bool `json:"skipMultilineEncoding"`
SecretValue string `json:"secretValue"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
opts := &SecretOptions{
Environment: req.Environment,
Path: req.SecretPath,
ProjectID: req.ProjectID,
}
secret, err := s.kms.CreateSecret(ctx, secretKey, []byte(req.SecretValue), opts)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// CreateSecretV3RawResponse format
s.writeJSON(w, http.StatusCreated, map[string]interface{}{
"secret": secretToResponse(secret, false, nil),
})
case "PATCH":
// UpdateSecretV3RawRequest format
var req struct {
ProjectID string `json:"workspaceId"`
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
Type string `json:"type"`
NewSecretValue string `json:"secretValue"`
NewSkipMultilineEncoding bool `json:"skipMultilineEncoding"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
// Find and update secret
secrets, err := s.kms.ListSecrets(ctx, req.Environment, req.SecretPath)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
for _, sec := range secrets {
if sec.Name == secretKey && (req.ProjectID == "" || sec.ProjectID == req.ProjectID) {
updatedSecret, err := s.kms.UpdateSecret(ctx, sec.ID, []byte(req.NewSecretValue))
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// UpdateSecretV3RawResponse format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"secret": secretToResponse(updatedSecret, false, nil),
})
return
}
}
s.writeError(w, http.StatusNotFound, "secret not found")
case "DELETE":
// DeleteSecretV3RawRequest query/body params
projectID := r.URL.Query().Get("workspaceId")
environment := r.URL.Query().Get("environment")
secretPath := r.URL.Query().Get("secretPath")
// Find and delete secret
secrets, err := s.kms.ListSecrets(ctx, environment, secretPath)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
for _, sec := range secrets {
if sec.Name == secretKey && (projectID == "" || sec.ProjectID == projectID) {
resp := secretToResponse(sec, false, nil)
if err := s.kms.DeleteSecret(ctx, sec.ID); err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
// DeleteSecretV3RawResponse format
s.writeJSON(w, http.StatusOK, map[string]interface{}{
"secret": resp,
})
return
}
}
s.writeError(w, http.StatusNotFound, "secret not found")
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleSecretsBatchV3(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
// BatchCreateSecretsV3RawRequest format
var req struct {
Environment string `json:"environment"`
ProjectID string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
Secrets []struct {
SecretKey string `json:"secretKey"`
SecretValue string `json:"secretValue"`
SecretComment string `json:"secretComment"`
SkipMultiLineEncoding bool `json:"skipMultilineEncoding"`
} `json:"secrets"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
createdSecrets := make([]SecretResponse, 0, len(req.Secrets))
for _, secReq := range req.Secrets {
opts := &SecretOptions{
Environment: req.Environment,
Path: req.SecretPath,
ProjectID: req.ProjectID,
}
secret, err := s.kms.CreateSecret(ctx, secReq.SecretKey, []byte(secReq.SecretValue), opts)
if err != nil {
s.writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create secret %s: %s", secReq.SecretKey, err.Error()))
return
}
createdSecrets = append(createdSecrets, secretToResponse(secret, false, nil))
}
// BatchCreateSecretsV3RawResponse format
s.writeJSON(w, http.StatusCreated, map[string]interface{}{
"secrets": createdSecrets,
})
}
// MPC handlers
func (s *Server) handleMPCNodes(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
switch r.Method {
case "GET":
nodes, err := s.mpc.ListNodes(ctx)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"nodes": nodes})
case "POST":
var req struct {
Name string `json:"name"`
Endpoint string `json:"endpoint"`
Port int `json:"port"`
PublicKey string `json:"publicKey"` // Base64
OrgID string `json:"orgId,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
publicKey, err := DecodeBase64(req.PublicKey)
if err != nil {
publicKey = []byte(req.PublicKey)
}
opts := &NodeOptions{
OrgID: req.OrgID,
Metadata: req.Metadata,
}
node, err := s.mpc.RegisterNode(ctx, req.Name, req.Endpoint, req.Port, publicKey, opts)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusCreated, map[string]interface{}{"node": node})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleMPCNode(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
nodeID := strings.TrimPrefix(r.URL.Path, "/v1/mpc/nodes/")
switch r.Method {
case "GET":
node, err := s.mpc.GetNode(ctx, nodeID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "node not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"node": node})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleMPCWallets(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
switch r.Method {
case "GET":
wallets, err := s.mpc.ListWallets(ctx)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"wallets": wallets})
case "POST":
var req struct {
Name string `json:"name"`
KeyType MPCKeyType `json:"keyType"`
Threshold int `json:"threshold"`
TotalParties int `json:"totalParties"`
ParticipantIDs []string `json:"participantIds"`
OrgID string `json:"orgId,omitempty"`
ProjectID string `json:"projectId,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
opts := &WalletOptions{
OrgID: req.OrgID,
ProjectID: req.ProjectID,
Metadata: req.Metadata,
}
wallet, err := s.mpc.CreateWallet(ctx, req.Name, req.KeyType, req.Threshold, req.TotalParties, req.ParticipantIDs, opts)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusCreated, map[string]interface{}{"wallet": wallet})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleMPCWallet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
walletID := strings.TrimPrefix(r.URL.Path, "/v1/mpc/wallets/")
switch r.Method {
case "GET":
wallet, err := s.mpc.GetWallet(ctx, walletID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "wallet not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"wallet": wallet})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleMPCSign(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
var req struct {
WalletID string `json:"walletId"`
Chain MPCChain `json:"chain"`
RawTransaction string `json:"rawTransaction"` // Base64
Message string `json:"message,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
rawTx, err := DecodeBase64(req.RawTransaction)
if err != nil {
rawTx = []byte(req.RawTransaction)
}
opts := &SigningOptions{
Metadata: req.Metadata,
}
if req.Message != "" {
opts.Message, _ = DecodeBase64(req.Message)
}
sigReq, err := s.mpc.CreateSigningRequest(ctx, req.WalletID, req.Chain, rawTx, opts)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusCreated, map[string]interface{}{"signingRequest": sigReq})
}
func (s *Server) handleMPCSigning(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
path := strings.TrimPrefix(r.URL.Path, "/v1/mpc/signing/")
// Check for /signature suffix
if strings.Contains(path, "/signature") {
requestID := strings.Split(path, "/")[0]
if r.Method == "POST" {
body, err := io.ReadAll(r.Body)
if err != nil {
s.writeError(w, http.StatusBadRequest, "failed to read body")
return
}
var req struct {
NodeID string `json:"nodeId"`
PartialSignature string `json:"partialSignature"` // Base64
}
if err := json.Unmarshal(body, &req); err != nil {
s.writeError(w, http.StatusBadRequest, "invalid request body")
return
}
sig, err := DecodeBase64(req.PartialSignature)
if err != nil {
s.writeError(w, http.StatusBadRequest, "invalid signature encoding")
return
}
sigReq, err := s.mpc.SubmitPartialSignature(ctx, requestID, req.NodeID, sig)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"signingRequest": sigReq})
return
}
}
requestID := path
switch r.Method {
case "GET":
sigReq, err := s.mpc.GetSigningRequest(ctx, requestID)
if err == ErrKeyNotFound {
s.writeError(w, http.StatusNotFound, "signing request not found")
return
}
if err != nil {
s.writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.writeJSON(w, http.StatusOK, map[string]interface{}{"signingRequest": sigReq})
default:
s.writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}