From f8728923a59c5151aa02ac866532e385914e499f Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 31 Oct 2024 21:15:38 +0100 Subject: [PATCH] Encrypt git token --- backend/cmd/server/main.go | 2 +- backend/internal/config/config.go | 23 +++++- backend/internal/crypto/crypto.go | 115 ++++++++++++++++++++++++++++++ backend/internal/db/db.go | 33 ++++++++- backend/internal/db/workspaces.go | 94 +++++++++++++++--------- 5 files changed, 230 insertions(+), 37 deletions(-) create mode 100644 backend/internal/crypto/crypto.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 83f440f..4effa3d 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -24,7 +24,7 @@ func main() { } // Initialize database - database, err := db.Init(cfg.DBPath) + database, err := db.Init(cfg.DBPath, cfg.EncryptionKey) if err != nil { log.Fatal(err) } diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 107ffd9..02257bf 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + + "novamd/internal/crypto" ) type Config struct { @@ -13,6 +15,7 @@ type Config struct { Port string AdminEmail string AdminPassword string + EncryptionKey string } func DefaultConfig() *Config { @@ -24,6 +27,19 @@ func DefaultConfig() *Config { } } +func (c *Config) Validate() error { + if c.AdminEmail == "" || c.AdminPassword == "" { + return fmt.Errorf("NOVAMD_ADMIN_EMAIL and NOVAMD_ADMIN_PASSWORD must be set") + } + + // Validate encryption key + if err := crypto.ValidateKey(c.EncryptionKey); err != nil { + return fmt.Errorf("invalid NOVAMD_ENCRYPTION_KEY: %w", err) + } + + return nil +} + // Load creates a new Config instance with values from environment variables func Load() (*Config, error) { config := DefaultConfig() @@ -52,8 +68,11 @@ func Load() (*Config, error) { config.AdminEmail = os.Getenv("NOVAMD_ADMIN_EMAIL") config.AdminPassword = os.Getenv("NOVAMD_ADMIN_PASSWORD") - if config.AdminEmail == "" || config.AdminPassword == "" { - return nil, fmt.Errorf("NOVAMD_ADMIN_EMAIL and NOVAMD_ADMIN_PASSWORD must be set") + config.EncryptionKey = os.Getenv("NOVAMD_ENCRYPTION_KEY") + + // Validate all settings + if err := config.Validate(); err != nil { + return nil, err } return config, nil diff --git a/backend/internal/crypto/crypto.go b/backend/internal/crypto/crypto.go new file mode 100644 index 0000000..1b08245 --- /dev/null +++ b/backend/internal/crypto/crypto.go @@ -0,0 +1,115 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" +) + +var ( + ErrKeyRequired = errors.New("encryption key is required") + ErrInvalidKeySize = errors.New("encryption key must be 32 bytes (256 bits) when decoded") +) + +type Crypto struct { + key []byte +} + +// ValidateKey checks if the provided key is suitable for AES-256 +func ValidateKey(key string) error { + if key == "" { + return ErrKeyRequired + } + + // Attempt to decode base64 + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return fmt.Errorf("invalid base64 encoding: %w", err) + } + + if len(keyBytes) != 32 { + return fmt.Errorf("%w: got %d bytes", ErrInvalidKeySize, len(keyBytes)) + } + + // Verify the key can be used for AES + _, err = aes.NewCipher(keyBytes) + if err != nil { + return fmt.Errorf("invalid encryption key: %w", err) + } + + return nil +} + +// New creates a new Crypto instance with the provided base64-encoded key +func New(key string) (*Crypto, error) { + if err := ValidateKey(key); err != nil { + return nil, err + } + + keyBytes, _ := base64.StdEncoding.DecodeString(key) + return &Crypto{key: keyBytes}, nil +} + +// Encrypt encrypts the plaintext using AES-256-GCM +func (c *Crypto) Encrypt(plaintext string) (string, error) { + if plaintext == "" { + return "", nil + } + + block, err := aes.NewCipher(c.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 + } + + ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt decrypts the ciphertext using AES-256-GCM +func (c *Crypto) Decrypt(ciphertext string) (string, error) { + if ciphertext == "" { + return "", nil + } + + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(c.key) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return "", errors.New("ciphertext too short") + } + + nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil) + if err != nil { + return "", err + } + + return string(plaintext), nil +} diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index 6389ff5..d176700 100644 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -2,15 +2,19 @@ package db import ( "database/sql" + "fmt" + + "novamd/internal/crypto" _ "github.com/mattn/go-sqlite3" ) type DB struct { *sql.DB + crypto *crypto.Crypto } -func Init(dbPath string) (*DB, error) { +func Init(dbPath string, encryptionKey string) (*DB, error) { db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, err @@ -20,7 +24,17 @@ func Init(dbPath string) (*DB, error) { return nil, err } - database := &DB{db} + // Initialize crypto service + cryptoService, err := crypto.New(encryptionKey) + if err != nil { + return nil, fmt.Errorf("failed to initialize encryption: %w", err) + } + + database := &DB{ + DB: db, + crypto: cryptoService, + } + if err := database.Migrate(); err != nil { return nil, err } @@ -31,3 +45,18 @@ func Init(dbPath string) (*DB, error) { func (db *DB) Close() error { return db.DB.Close() } + +// Helper methods for token encryption/decryption +func (db *DB) encryptToken(token string) (string, error) { + if token == "" { + return "", nil + } + return db.crypto.Encrypt(token) +} + +func (db *DB) decryptToken(token string) (string, error) { + if token == "" { + return "", nil + } + return db.crypto.Decrypt(token) +} diff --git a/backend/internal/db/workspaces.go b/backend/internal/db/workspaces.go index b3264ae..15944ec 100644 --- a/backend/internal/db/workspaces.go +++ b/backend/internal/db/workspaces.go @@ -2,6 +2,7 @@ package db import ( "database/sql" + "fmt" "novamd/internal/models" ) @@ -11,6 +12,12 @@ func (db *DB) CreateWorkspace(workspace *models.Workspace) error { workspace.GetDefaultSettings() } + // Encrypt token if present + encryptedToken, err := db.encryptToken(workspace.GitToken) + if err != nil { + return fmt.Errorf("failed to encrypt token: %w", err) + } + result, err := db.Exec(` INSERT INTO workspaces ( user_id, name, theme, auto_save, @@ -18,7 +25,7 @@ func (db *DB) CreateWorkspace(workspace *models.Workspace) error { git_auto_commit, git_commit_msg_template ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, - workspace.GitEnabled, workspace.GitURL, workspace.GitUser, workspace.GitToken, + workspace.GitEnabled, workspace.GitURL, workspace.GitUser, encryptedToken, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, ) if err != nil { @@ -35,6 +42,8 @@ func (db *DB) CreateWorkspace(workspace *models.Workspace) error { func (db *DB) GetWorkspaceByID(id int) (*models.Workspace, error) { workspace := &models.Workspace{} + var encryptedToken string + err := db.QueryRow(` SELECT id, user_id, name, created_at, @@ -47,15 +56,57 @@ func (db *DB) GetWorkspaceByID(id int) (*models.Workspace, error) { ).Scan( &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, &workspace.Theme, &workspace.AutoSave, - &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &workspace.GitToken, + &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, ) if err != nil { return nil, err } + + // Decrypt token + workspace.GitToken, err = db.decryptToken(encryptedToken) + if err != nil { + return nil, fmt.Errorf("failed to decrypt token: %w", err) + } + return workspace, nil } +func (db *DB) UpdateWorkspace(workspace *models.Workspace) error { + // Encrypt token before storing + encryptedToken, err := db.encryptToken(workspace.GitToken) + if err != nil { + return fmt.Errorf("failed to encrypt token: %w", err) + } + + _, err = db.Exec(` + UPDATE workspaces + SET + name = ?, + theme = ?, + auto_save = ?, + git_enabled = ?, + git_url = ?, + git_user = ?, + git_token = ?, + git_auto_commit = ?, + git_commit_msg_template = ? + WHERE id = ? AND user_id = ?`, + workspace.Name, + workspace.Theme, + workspace.AutoSave, + workspace.GitEnabled, + workspace.GitURL, + workspace.GitUser, + encryptedToken, + workspace.GitAutoCommit, + workspace.GitCommitMsgTemplate, + workspace.ID, + workspace.UserID, + ) + return err +} + func (db *DB) GetWorkspacesByUserID(userID int) ([]*models.Workspace, error) { rows, err := db.Query(` SELECT @@ -75,49 +126,28 @@ func (db *DB) GetWorkspacesByUserID(userID int) ([]*models.Workspace, error) { var workspaces []*models.Workspace for rows.Next() { workspace := &models.Workspace{} + var encryptedToken string err := rows.Scan( &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, &workspace.Theme, &workspace.AutoSave, - &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &workspace.GitToken, + &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, ) if err != nil { return nil, err } + + // Decrypt token + workspace.GitToken, err = db.decryptToken(encryptedToken) + if err != nil { + return nil, fmt.Errorf("failed to decrypt token: %w", err) + } + workspaces = append(workspaces, workspace) } return workspaces, nil } -func (db *DB) UpdateWorkspace(workspace *models.Workspace) error { - _, err := db.Exec(` - UPDATE workspaces - SET - name = ?, - theme = ?, - auto_save = ?, - git_enabled = ?, - git_url = ?, - git_user = ?, - git_token = ?, - git_auto_commit = ?, - git_commit_msg_template = ? - WHERE id = ? AND user_id = ?`, - workspace.Name, - workspace.Theme, - workspace.AutoSave, - workspace.GitEnabled, - workspace.GitURL, - workspace.GitUser, - workspace.GitToken, - workspace.GitAutoCommit, - workspace.GitCommitMsgTemplate, - workspace.ID, - workspace.UserID, - ) - return err -} - // UpdateWorkspaceSettings updates only the settings portion of a workspace // This is useful when you don't want to modify the name or other core workspace properties func (db *DB) UpdateWorkspaceSettings(workspace *models.Workspace) error {