@@ -0,0 +1,72 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func encryptionKey() ([]byte, error) {
|
||||
raw := os.Getenv("KEY_ENCRYPTION_KEY")
|
||||
if raw == "" {
|
||||
return nil, fmt.Errorf("KEY_ENCRYPTION_KEY is not set")
|
||||
}
|
||||
key, err := hex.DecodeString(raw)
|
||||
if err != nil || len(key) != 32 {
|
||||
return nil, fmt.Errorf("KEY_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func encryptPrivateKey(plaintext string) (string, error) {
|
||||
key, err := encryptionKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
sealed := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return hex.EncodeToString(sealed), nil
|
||||
}
|
||||
|
||||
func decryptPrivateKey(ciphertextHex string) (string, error) {
|
||||
key, err := encryptionKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := hex.DecodeString(ciphertextHex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid ciphertext encoding")
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
plaintext, err := gcm.Open(nil, data[:nonceSize], data[nonceSize:], nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decryption failed")
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
@@ -31,7 +31,11 @@ func computeFingerprint(pubKey string) string {
|
||||
return "MD5:" + strings.Join(pairs, ":")
|
||||
}
|
||||
|
||||
func CreateKey(label, publicKey, source, generatedByServerID string) (*models.Key, error) {
|
||||
func setKeyMeta(k *models.Key) {
|
||||
k.HasPrivateKey = k.PrivateKeyEncrypted != ""
|
||||
}
|
||||
|
||||
func CreateKey(label, publicKey, source, generatedByServerID, privateKey string) (*models.Key, error) {
|
||||
key := &models.Key{
|
||||
KeyID: uuid.NewString(),
|
||||
Label: label,
|
||||
@@ -41,6 +45,13 @@ func CreateKey(label, publicKey, source, generatedByServerID string) (*models.Ke
|
||||
GeneratedByServerID: generatedByServerID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if privateKey != "" {
|
||||
enc, err := encryptPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encrypt private key: %w", err)
|
||||
}
|
||||
key.PrivateKeyEncrypted = enc
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -48,6 +59,7 @@ func CreateKey(label, publicKey, source, generatedByServerID string) (*models.Ke
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setKeyMeta(key)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@@ -60,9 +72,24 @@ func GetKey(keyID string) (*models.Key, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setKeyMeta(&key)
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func GetPrivateKey(keyID string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var key models.Key
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID}).Decode(&key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if key.PrivateKeyEncrypted == "" {
|
||||
return "", fmt.Errorf("no private key stored for this key")
|
||||
}
|
||||
return decryptPrivateKey(key.PrivateKeyEncrypted)
|
||||
}
|
||||
|
||||
type KeyWithCount struct {
|
||||
models.Key `bson:",inline"`
|
||||
AssignedCount int `bson:"-" json:"assigned_count"`
|
||||
@@ -85,6 +112,7 @@ func ListKeys() ([]KeyWithCount, error) {
|
||||
|
||||
result := make([]KeyWithCount, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
setKeyMeta(&k)
|
||||
count, _ := db.Col("assignments").CountDocuments(ctx, bson.M{
|
||||
"key_id": k.KeyID,
|
||||
"revoked_at": nil,
|
||||
@@ -219,6 +247,7 @@ func GetAssignmentsWithKeysForServer(serverID string) ([]AssignmentWithKey, erro
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID}).Decode(&key); err != nil {
|
||||
continue
|
||||
}
|
||||
setKeyMeta(&key)
|
||||
result = append(result, AssignmentWithKey{Assignment: a, Key: &key})
|
||||
}
|
||||
return result, nil
|
||||
|
||||
Reference in New Issue
Block a user