package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "errors" "fmt" "io" "os" ) const moduleKeyEnv = "KANBAN_MODULE_KEY" // moduleKey derives a 32-byte AES key from the KANBAN_MODULE_KEY env var. // Returns (key, true) when present; (zero, false) when missing — callers // must treat that as "module dispatcher disabled". func moduleKey() ([32]byte, bool) { v := os.Getenv(moduleKeyEnv) if v == "" { return [32]byte{}, false } return sha256.Sum256([]byte(v)), true } // encryptConfig encrypts a JSON config blob with AES-GCM. Returns the // ciphertext and the 12-byte nonce. Caller persists both columns. func encryptConfig(plain []byte) (cipherOut, nonce []byte, err error) { key, ok := moduleKey() if !ok { return nil, nil, fmt.Errorf("%s not set; cannot encrypt module config", moduleKeyEnv) } block, err := aes.NewCipher(key[:]) if err != nil { return nil, nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, nil, err } nonce = make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, nil, err } cipherOut = gcm.Seal(nil, nonce, plain, nil) return cipherOut, nonce, nil } // decryptConfig is the inverse of encryptConfig. func decryptConfig(cipherIn, nonce []byte) ([]byte, error) { key, ok := moduleKey() if !ok { return nil, fmt.Errorf("%s not set; cannot decrypt module config", moduleKeyEnv) } if len(nonce) == 0 { return nil, errors.New("nonce empty") } block, err := aes.NewCipher(key[:]) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } return gcm.Open(nil, nonce, cipherIn, nil) }