99 lines
2.6 KiB
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
|
|
}
|
|
}
|