|  | 
|  | 1 | +// Package encrypt provides reversible AES-256-GCM encryption and decryption utilities | 
|  | 2 | +// for securing sensitive data like API keys and credentials. | 
|  | 3 | +package encrypt | 
|  | 4 | + | 
|  | 5 | +import ( | 
|  | 6 | +	"crypto/aes" | 
|  | 7 | +	"crypto/cipher" | 
|  | 8 | +	"crypto/rand" | 
|  | 9 | +	"encoding/base64" | 
|  | 10 | +	"fmt" | 
|  | 11 | +	"io" | 
|  | 12 | +	"strings" | 
|  | 13 | + | 
|  | 14 | +	"github.com/maximhq/bifrost/core/schemas" | 
|  | 15 | +) | 
|  | 16 | + | 
|  | 17 | +var encryptionKey []byte | 
|  | 18 | +var logger schemas.Logger | 
|  | 19 | + | 
|  | 20 | +const DefaultKey = "bifrost-default-encryption-key-32b" | 
|  | 21 | + | 
|  | 22 | +// Init initializes the encryption key from environment variable | 
|  | 23 | +func Init(key string, _logger schemas.Logger) { | 
|  | 24 | +	logger = _logger | 
|  | 25 | +	if key == "" { | 
|  | 26 | +		// In this case encryption will be disabled | 
|  | 27 | +		logger.Warn("encryption key is not set, encryption will be disabled. To set encryption key: use the encryption_key field in the configuration file or set the BIFROST_ENCRYPTION_KEY environment variable. Note that - once encryption key is set, it cannot be changed later unless you clean up the database.") | 
|  | 28 | +		return | 
|  | 29 | +	} | 
|  | 30 | +	// Ensure key is exactly 32 bytes for AES-256 | 
|  | 31 | +	if len(key) < 32 { | 
|  | 32 | +		// Pad with zeros if too short | 
|  | 33 | +		encryptionKey = make([]byte, 32) | 
|  | 34 | +		copy(encryptionKey, []byte(key)) | 
|  | 35 | +	} else { | 
|  | 36 | +		// Truncate if too long | 
|  | 37 | +		encryptionKey = []byte(key)[:32] | 
|  | 38 | +	} | 
|  | 39 | +} | 
|  | 40 | + | 
|  | 41 | +// IsRedacted checks if a secret is redacted | 
|  | 42 | +func IsRedacted(secret string) bool { | 
|  | 43 | +	if secret == "" { | 
|  | 44 | +		return false | 
|  | 45 | +	} | 
|  | 46 | + | 
|  | 47 | +	// Check if completely redacted (all asterisks) | 
|  | 48 | +	if secret == strings.Repeat("*", len(secret)) { | 
|  | 49 | +		return true | 
|  | 50 | +	} | 
|  | 51 | + | 
|  | 52 | +	// Check if partially redacted (starts with asterisks, ends with visible chars) | 
|  | 53 | +	if len(secret) > 4 && strings.HasPrefix(secret, strings.Repeat("*", len(secret)-4)) { | 
|  | 54 | +		return true | 
|  | 55 | +	} | 
|  | 56 | + | 
|  | 57 | +	return false | 
|  | 58 | +} | 
|  | 59 | + | 
|  | 60 | +// RedactSecret redacts a secret string, keeping the last 4 characters visible | 
|  | 61 | +func RedactSecret(secret string) string { | 
|  | 62 | +	if secret == "" { | 
|  | 63 | +		return "" | 
|  | 64 | +	} | 
|  | 65 | +	length := len(secret) | 
|  | 66 | +	if length <= 4 { | 
|  | 67 | +		// If secret is 4 chars or less, redact completely | 
|  | 68 | +		return strings.Repeat("*", length) | 
|  | 69 | +	} | 
|  | 70 | +	// Show last 4 characters | 
|  | 71 | +	visiblePart := secret[length-4:] | 
|  | 72 | +	redactedPart := strings.Repeat("*", length-4) | 
|  | 73 | +	return redactedPart + visiblePart | 
|  | 74 | +} | 
|  | 75 | + | 
|  | 76 | +// Encrypt encrypts a plaintext string using AES-256-GCM and returns a base64-encoded ciphertext | 
|  | 77 | +func Encrypt(plaintext string) string { | 
|  | 78 | +	if encryptionKey == nil { | 
|  | 79 | +		return plaintext | 
|  | 80 | +	} | 
|  | 81 | +	if plaintext == "" { | 
|  | 82 | +		return "" | 
|  | 83 | +	} | 
|  | 84 | + | 
|  | 85 | +	block, err := aes.NewCipher(encryptionKey) | 
|  | 86 | +	if err != nil { | 
|  | 87 | +		return plaintext // Fallback to plaintext on error | 
|  | 88 | +	} | 
|  | 89 | + | 
|  | 90 | +	aesGCM, err := cipher.NewGCM(block) | 
|  | 91 | +	if err != nil { | 
|  | 92 | +		return plaintext | 
|  | 93 | +	} | 
|  | 94 | + | 
|  | 95 | +	// Create a nonce (number used once) | 
|  | 96 | +	nonce := make([]byte, aesGCM.NonceSize()) | 
|  | 97 | +	if _, err := io.ReadFull(rand.Reader, nonce); err != nil { | 
|  | 98 | +		return plaintext | 
|  | 99 | +	} | 
|  | 100 | + | 
|  | 101 | +	// Encrypt the data | 
|  | 102 | +	ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil) | 
|  | 103 | + | 
|  | 104 | +	// Encode to base64 for storage | 
|  | 105 | +	return base64.StdEncoding.EncodeToString(ciphertext) | 
|  | 106 | +} | 
|  | 107 | + | 
|  | 108 | +// Decrypt decrypts a base64-encoded ciphertext using AES-256-GCM and returns the plaintext | 
|  | 109 | +func Decrypt(ciphertext string) (string, error) { | 
|  | 110 | +	if encryptionKey == nil { | 
|  | 111 | +		return ciphertext, nil | 
|  | 112 | +	} | 
|  | 113 | +	if ciphertext == "" { | 
|  | 114 | +		return "", nil | 
|  | 115 | +	} | 
|  | 116 | + | 
|  | 117 | +	// Decode from base64 | 
|  | 118 | +	data, err := base64.StdEncoding.DecodeString(ciphertext) | 
|  | 119 | +	if err != nil { | 
|  | 120 | +		return "", fmt.Errorf("failed to decode base64: %w", err) | 
|  | 121 | +	} | 
|  | 122 | + | 
|  | 123 | +	block, err := aes.NewCipher(encryptionKey) | 
|  | 124 | +	if err != nil { | 
|  | 125 | +		return "", fmt.Errorf("failed to create cipher: %w", err) | 
|  | 126 | +	} | 
|  | 127 | + | 
|  | 128 | +	aesGCM, err := cipher.NewGCM(block) | 
|  | 129 | +	if err != nil { | 
|  | 130 | +		return "", fmt.Errorf("failed to create GCM: %w", err) | 
|  | 131 | +	} | 
|  | 132 | + | 
|  | 133 | +	// Extract nonce | 
|  | 134 | +	nonceSize := aesGCM.NonceSize() | 
|  | 135 | +	if len(data) < nonceSize { | 
|  | 136 | +		return "", fmt.Errorf("ciphertext too short") | 
|  | 137 | +	} | 
|  | 138 | + | 
|  | 139 | +	nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:] | 
|  | 140 | + | 
|  | 141 | +	// Decrypt the data | 
|  | 142 | +	plaintext, err := aesGCM.Open(nil, nonce, ciphertextBytes, nil) | 
|  | 143 | +	if err != nil { | 
|  | 144 | +		return "", fmt.Errorf("failed to decrypt: %w", err) | 
|  | 145 | +	} | 
|  | 146 | + | 
|  | 147 | +	return string(plaintext), nil | 
|  | 148 | +} | 
0 commit comments