376 lines
12 KiB
Go
376 lines
12 KiB
Go
// Keywarden - Centralized SSH Key Management and Deployment
|
|
// Copyright (C) 2026 Patrick Asmus (scriptos)
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package keys
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.techniverse.net/scriptos/keywarden/internal/database"
|
|
"git.techniverse.net/scriptos/keywarden/internal/encryption"
|
|
"git.techniverse.net/scriptos/keywarden/internal/models"
|
|
"git.techniverse.net/scriptos/keywarden/internal/sshutil"
|
|
)
|
|
|
|
// Service handles SSH key operations
|
|
type Service struct {
|
|
db *database.DB
|
|
enc *encryption.Service
|
|
}
|
|
|
|
// NewService creates a new key service with encryption
|
|
func NewService(db *database.DB, enc *encryption.Service) *Service {
|
|
return &Service{db: db, enc: enc}
|
|
}
|
|
|
|
// GenerateKey generates a new SSH key pair and stores it encrypted
|
|
func (s *Service) GenerateKey(userID int64, name, keyType string, bits int, comment string) (*models.SSHKey, error) {
|
|
return s.generateKey(userID, name, keyType, bits, comment)
|
|
}
|
|
|
|
// generateKey is the internal key generation function
|
|
func (s *Service) generateKey(userID int64, name, keyType string, bits int, comment string) (*models.SSHKey, error) {
|
|
var privPEM, pubKey []byte
|
|
var fingerprint string
|
|
var err error
|
|
|
|
switch keyType {
|
|
case "rsa":
|
|
privPEM, pubKey, fingerprint, err = sshutil.GenerateRSAKey(bits, comment)
|
|
case "ed25519":
|
|
privPEM, pubKey, fingerprint, err = sshutil.GenerateEd25519Key(comment)
|
|
bits = 256
|
|
case "ed448":
|
|
privPEM, pubKey, fingerprint, err = sshutil.GenerateEd448Key(comment)
|
|
bits = 456
|
|
default:
|
|
return nil, fmt.Errorf("unsupported key type: %s", keyType)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate key: %w", err)
|
|
}
|
|
|
|
// Encrypt private key before storage
|
|
encPrivKey, err := s.enc.Encrypt(string(privPEM))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
|
|
}
|
|
|
|
result, err := s.db.Exec(
|
|
`INSERT INTO ssh_keys (user_id, name, key_type, bits, fingerprint, public_key, private_key_enc)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
userID, name, keyType, bits, fingerprint, string(pubKey), encPrivKey,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to store key: %w", err)
|
|
}
|
|
|
|
id, _ := result.LastInsertId()
|
|
return &models.SSHKey{
|
|
ID: id,
|
|
UserID: userID,
|
|
Name: name,
|
|
KeyType: keyType,
|
|
Bits: bits,
|
|
Fingerprint: fingerprint,
|
|
PublicKey: string(pubKey),
|
|
}, nil
|
|
}
|
|
|
|
// ImportKey imports an existing key pair (encrypts the private key)
|
|
func (s *Service) ImportKey(userID int64, name string, privateKeyPEM []byte) (*models.SSHKey, error) {
|
|
pubKey, fingerprint, keyType, err := sshutil.ParsePrivateKey(privateKeyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
|
|
// Map SSH key type names
|
|
kt := "rsa"
|
|
bits := 0
|
|
switch keyType {
|
|
case "ssh-rsa":
|
|
kt = "rsa"
|
|
bits = 2048 // approximate
|
|
case "ssh-ed25519":
|
|
kt = "ed25519"
|
|
bits = 256
|
|
case "ssh-ed448":
|
|
kt = "ed448"
|
|
bits = 456
|
|
}
|
|
|
|
// Encrypt private key before storage
|
|
encPrivKey, err := s.enc.Encrypt(string(privateKeyPEM))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
|
|
}
|
|
|
|
result, err := s.db.Exec(
|
|
`INSERT INTO ssh_keys (user_id, name, key_type, bits, fingerprint, public_key, private_key_enc)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
userID, name, kt, bits, fingerprint, string(pubKey), encPrivKey,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to store key: %w", err)
|
|
}
|
|
|
|
id, _ := result.LastInsertId()
|
|
return &models.SSHKey{
|
|
ID: id,
|
|
UserID: userID,
|
|
Name: name,
|
|
KeyType: kt,
|
|
Bits: bits,
|
|
Fingerprint: fingerprint,
|
|
PublicKey: string(pubKey),
|
|
}, nil
|
|
}
|
|
|
|
// GetKeysByUser returns all keys for a user
|
|
func (s *Service) GetKeysByUser(userID int64) ([]models.SSHKey, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, user_id, name, key_type, bits, fingerprint, public_key, created_at
|
|
FROM ssh_keys WHERE user_id = ? ORDER BY created_at DESC`, userID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query keys: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var keys []models.SSHKey
|
|
for rows.Next() {
|
|
var k models.SSHKey
|
|
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyType, &k.Bits, &k.Fingerprint, &k.PublicKey, &k.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("failed to scan key: %w", err)
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// GetAllKeys returns all SSH keys for all users (admin use)
|
|
func (s *Service) GetAllKeys() ([]models.SSHKey, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, user_id, name, key_type, bits, fingerprint, public_key, created_at
|
|
FROM ssh_keys ORDER BY user_id, name ASC`,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query all keys: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var keys []models.SSHKey
|
|
for rows.Next() {
|
|
var k models.SSHKey
|
|
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyType, &k.Bits, &k.Fingerprint, &k.PublicKey, &k.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("failed to scan key: %w", err)
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// GetKeyByID returns a specific key with decrypted private key
|
|
func (s *Service) GetKeyByID(keyID, userID int64) (*models.SSHKey, error) {
|
|
key := &models.SSHKey{}
|
|
var encPrivKey string
|
|
err := s.db.QueryRow(
|
|
`SELECT id, user_id, name, key_type, bits, fingerprint, public_key, private_key_enc, created_at
|
|
FROM ssh_keys WHERE id = ? AND user_id = ?`, keyID, userID,
|
|
).Scan(&key.ID, &key.UserID, &key.Name, &key.KeyType, &key.Bits, &key.Fingerprint, &key.PublicKey, &encPrivKey, &key.CreatedAt)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("key not found: %w", err)
|
|
}
|
|
|
|
// Decrypt private key
|
|
decrypted, err := s.enc.Decrypt(encPrivKey)
|
|
if err != nil {
|
|
// Fallback: might be an old unencrypted key
|
|
key.PrivateKeyEnc = encPrivKey
|
|
} else {
|
|
key.PrivateKeyEnc = decrypted
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// DeleteKey deletes a key
|
|
func (s *Service) DeleteKey(keyID, userID int64) error {
|
|
result, err := s.db.Exec(`DELETE FROM ssh_keys WHERE id = ? AND user_id = ?`, keyID, userID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete key: %w", err)
|
|
}
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("key not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAllKeysWithOwner returns all SSH keys with their owner's username (for admin/owner views)
|
|
func (s *Service) GetAllKeysWithOwner() ([]models.SSHKeyWithOwner, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT k.id, k.user_id, k.name, k.key_type, k.bits, k.fingerprint, k.public_key, k.created_at,
|
|
COALESCE(u.username, '(deleted)')
|
|
FROM ssh_keys k
|
|
LEFT JOIN users u ON k.user_id = u.id
|
|
ORDER BY u.username ASC, k.name ASC`,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query all keys with owner: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var keys []models.SSHKeyWithOwner
|
|
for rows.Next() {
|
|
var k models.SSHKeyWithOwner
|
|
if err := rows.Scan(&k.ID, &k.UserID, &k.Name, &k.KeyType, &k.Bits, &k.Fingerprint, &k.PublicKey, &k.CreatedAt, &k.OwnerUsername); err != nil {
|
|
return nil, fmt.Errorf("failed to scan key: %w", err)
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// GetKeyByIDGlobal returns a specific key without user_id check (admin/owner access)
|
|
// Note: Private key is NOT returned decrypted — only metadata and public key
|
|
func (s *Service) GetKeyByIDGlobal(keyID int64) (*models.SSHKey, error) {
|
|
key := &models.SSHKey{}
|
|
err := s.db.QueryRow(
|
|
`SELECT id, user_id, name, key_type, bits, fingerprint, public_key, created_at
|
|
FROM ssh_keys WHERE id = ?`, keyID,
|
|
).Scan(&key.ID, &key.UserID, &key.Name, &key.KeyType, &key.Bits, &key.Fingerprint, &key.PublicKey, &key.CreatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("key not found: %w", err)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// DeleteKeyGlobal deletes a key without user_id check (admin/owner access)
|
|
func (s *Service) DeleteKeyGlobal(keyID int64) error {
|
|
result, err := s.db.Exec(`DELETE FROM ssh_keys WHERE id = ?`, keyID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete key: %w", err)
|
|
}
|
|
rows, _ := result.RowsAffected()
|
|
if rows == 0 {
|
|
return fmt.Errorf("key not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- System Master Key ---
|
|
// The system master key is an Ed25519 key pair used by Keywarden to authenticate
|
|
// against remote servers for key deployments. It is generated once on first startup
|
|
// and stored encrypted in the settings table. It cannot be deleted, only regenerated.
|
|
|
|
// EnsureSystemMasterKey generates the system master key if it doesn't exist yet.
|
|
// Returns the public key string.
|
|
func (s *Service) EnsureSystemMasterKey() (string, error) {
|
|
pub, err := s.getSetting("system_master_key_public")
|
|
if err == nil && pub != "" {
|
|
return pub, nil
|
|
}
|
|
// Generate new master key
|
|
return s.generateSystemMasterKey()
|
|
}
|
|
|
|
// generateSystemMasterKey generates a new Ed25519 key pair and stores it in settings.
|
|
func (s *Service) generateSystemMasterKey() (string, error) {
|
|
privPEM, pubKey, fingerprint, err := sshutil.GenerateEd25519Key("keywarden-system-master")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate system master key: %w", err)
|
|
}
|
|
|
|
// Encrypt private key
|
|
encPriv, err := s.enc.Encrypt(string(privPEM))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to encrypt system master key: %w", err)
|
|
}
|
|
|
|
pubStr := string(pubKey)
|
|
|
|
// Store in settings table
|
|
if err := s.setSetting("system_master_key_private", encPriv); err != nil {
|
|
return "", fmt.Errorf("failed to store system master key private: %w", err)
|
|
}
|
|
if err := s.setSetting("system_master_key_public", pubStr); err != nil {
|
|
return "", fmt.Errorf("failed to store system master key public: %w", err)
|
|
}
|
|
if err := s.setSetting("system_master_key_fingerprint", fingerprint); err != nil {
|
|
return "", fmt.Errorf("failed to store system master key fingerprint: %w", err)
|
|
}
|
|
|
|
return pubStr, nil
|
|
}
|
|
|
|
// GetSystemMasterKeyPublic returns the public key of the system master key.
|
|
func (s *Service) GetSystemMasterKeyPublic() (string, error) {
|
|
pub, err := s.getSetting("system_master_key_public")
|
|
if err != nil || pub == "" {
|
|
return "", fmt.Errorf("system master key not found")
|
|
}
|
|
return pub, nil
|
|
}
|
|
|
|
// GetSystemMasterKeyFingerprint returns the fingerprint of the system master key.
|
|
func (s *Service) GetSystemMasterKeyFingerprint() (string, error) {
|
|
fp, err := s.getSetting("system_master_key_fingerprint")
|
|
if err != nil || fp == "" {
|
|
return "", fmt.Errorf("system master key fingerprint not found")
|
|
}
|
|
return fp, nil
|
|
}
|
|
|
|
// GetSystemMasterKeyPrivate returns the decrypted private key PEM of the system master key.
|
|
func (s *Service) GetSystemMasterKeyPrivate() ([]byte, error) {
|
|
encPriv, err := s.getSetting("system_master_key_private")
|
|
if err != nil || encPriv == "" {
|
|
return nil, fmt.Errorf("system master key not found")
|
|
}
|
|
decrypted, err := s.enc.Decrypt(encPriv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt system master key: %w", err)
|
|
}
|
|
return []byte(decrypted), nil
|
|
}
|
|
|
|
// RegenerateSystemMasterKey generates a new system master key, replacing the old one.
|
|
// Returns the new public key string.
|
|
func (s *Service) RegenerateSystemMasterKey() (string, error) {
|
|
return s.generateSystemMasterKey()
|
|
}
|
|
|
|
// getSetting reads a value from the settings table.
|
|
func (s *Service) getSetting(key string) (string, error) {
|
|
var value string
|
|
err := s.db.QueryRow(`SELECT value FROM settings WHERE key = ?`, key).Scan(&value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// setSetting writes a value to the settings table (upsert).
|
|
func (s *Service) setSetting(key, value string) error {
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`,
|
|
key, value,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// EncryptValue encrypts a plaintext string using the application encryption key
|
|
func (s *Service) EncryptValue(plaintext string) (string, error) {
|
|
return s.enc.Encrypt(plaintext)
|
|
}
|
|
|
|
// DecryptValue decrypts an encrypted string using the application encryption key
|
|
func (s *Service) DecryptValue(ciphertext string) (string, error) {
|
|
return s.enc.Decrypt(ciphertext)
|
|
}
|