supabase-cli/pkg/config/secret.go

99 lines
2.6 KiB
Go

package config
import (
"encoding/base64"
"os"
"reflect"
"strings"
ecies "github.com/ecies/go/v2"
"github.com/go-errors/errors"
"github.com/mitchellh/mapstructure"
)
type Secret struct {
Value string
SHA256 string
}
const HASHED_PREFIX = "hash:"
func (s Secret) MarshalText() (text []byte, err error) {
if len(s.SHA256) == 0 {
return []byte{}, nil
}
return []byte(HASHED_PREFIX + s.SHA256), nil
}
const ENCRYPTED_PREFIX = "encrypted:"
// Decrypt secret values following dotenvx convention:
// https://github.com/dotenvx/dotenvx/blob/main/src/lib/helpers/decryptKeyValue.js
func decrypt(key, value string) (string, error) {
if !strings.HasPrefix(value, ENCRYPTED_PREFIX) {
return value, nil
}
if len(key) == 0 {
return value, errors.New("missing private key")
}
// Verify private key exists
privateKey, err := ecies.NewPrivateKeyFromHex(key)
if err != nil {
return value, errors.Errorf("failed to hex decode private key: %w", err)
}
// Verify ciphertext is base64 encoded
encoded := value[len(ENCRYPTED_PREFIX):]
ciphertext, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return value, errors.Errorf("failed to base64 decode secret: %w", err)
}
// Return decrypted value
plaintext, err := ecies.Decrypt(privateKey, ciphertext)
if err != nil {
return value, errors.Errorf("failed to decrypt secret: %w", err)
}
return string(plaintext), nil
}
func DecryptSecretHookFunc(hashKey string) mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
var result Secret
if t != reflect.TypeOf(result) {
return data, nil
}
value := data.(string)
if len(value) == 0 {
return result, nil
}
// Get all env vars and filter for DOTENV_PRIVATE_KEY
var privateKeys []string
for _, env := range os.Environ() {
key := strings.Split(env, "=")[0]
if key == "DOTENV_PRIVATE_KEY" || strings.HasPrefix(key, "DOTENV_PRIVATE_KEY_") {
if value := os.Getenv(key); value != "" {
privateKeys = append(privateKeys, value)
}
}
}
// Try each private key
var err error
privKey := strings.Join(privateKeys, ",")
for _, k := range strings.Split(privKey, ",") {
// Use the first private key that successfully decrypts the secret
if result.Value, err = decrypt(k, value); err == nil {
// Unloaded env() references may be returned verbatim.
// Don't hash those values as they are meaningless.
if !envPattern.MatchString(result.Value) {
result.SHA256 = sha256Hmac(hashKey, result.Value)
}
break
}
}
// If we get here, none of the keys worked
return result, err
}
}