From 6ada50580955623dda4f4caaccd858753a68cca2 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Thu, 2 Jan 2025 15:08:26 +0800 Subject: [PATCH] feat: support encrypted values in config --- go.mod | 7 +++-- go.sum | 12 +++++++-- pkg/config/secret.go | 57 +++++++++++++++++++++++++++++++++++++++ pkg/config/secret_test.go | 52 +++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 pkg/config/secret.go create mode 100644 pkg/config/secret_test.go diff --git a/go.mod b/go.mod index 2a7b49ac3..03bb4e3a4 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/docker/docker v27.4.1+incompatible github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 + github.com/ecies/go/v2 v2.0.10 github.com/getsentry/sentry-go v0.31.1 github.com/go-errors/errors v1.5.1 github.com/go-git/go-git/v5 v5.13.1 @@ -111,6 +112,7 @@ require ( github.com/daixiang0/gci v0.13.5 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect @@ -120,6 +122,7 @@ require ( github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/ethereum/go-ethereum v1.14.12 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -215,7 +218,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mgechev/revive v1.5.1 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect @@ -244,7 +247,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.7.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect diff --git a/go.sum b/go.sum index 9b19db7e3..279f29fae 100644 --- a/go.sum +++ b/go.sum @@ -215,6 +215,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/deepmap/oapi-codegen/v2 v2.2.0 h1:FW4f7C0Xb6EaezBSB3GYw2QGwHD5ChDflG+3xSZBdvY= github.com/deepmap/oapi-codegen/v2 v2.2.0/go.mod h1:L4zUv7ULYDtYSb/aYk/xO3OYcQU6BoU/0viULkbi2DE= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= @@ -248,6 +250,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/ecies/go/v2 v2.0.10 h1:AaLxGio0MLLbvWur4rKnLzw+K9zI+wMScIDAtqCqOtU= +github.com/ecies/go/v2 v2.0.10/go.mod h1:N73OyuR6tuKznit2LhXjrZ0XAQ234uKbzYz8pEPYzlI= github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -257,6 +261,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4= +github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= @@ -678,8 +684,9 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgechev/revive v1.5.1 h1:hE+QPeq0/wIzJwOphdVyUJ82njdd8Khp4fUIHGZHW3M= github.com/mgechev/revive v1.5.1/go.mod h1:lC9AhkJIBs5zwx8wkudyHrU+IJkrEKmpCmGMnIJPk4o= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= @@ -792,8 +799,9 @@ github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1: github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= diff --git a/pkg/config/secret.go b/pkg/config/secret.go new file mode 100644 index 000000000..73e545133 --- /dev/null +++ b/pkg/config/secret.go @@ -0,0 +1,57 @@ +package config + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + + ecies "github.com/ecies/go/v2" + "github.com/go-errors/errors" +) + +type Secret string + +func (s Secret) PlainText() *string { + key := os.Getenv("DOTENV_PRIVATE_KEY") + for _, k := range strings.Split(key, ",") { + value, err := decrypt(k, string(s)) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } else if len(value) > 0 { + return &value + } + } + // Empty strings are converted to nil + return 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 +} diff --git a/pkg/config/secret_test.go b/pkg/config/secret_test.go new file mode 100644 index 000000000..c4bf74e5b --- /dev/null +++ b/pkg/config/secret_test.go @@ -0,0 +1,52 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecryptSecret(t *testing.T) { + key := "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb" + value := "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/" + + t.Run("decrypts secret value", func(t *testing.T) { + // Run test + plaintext, err := decrypt(key, value) + // Check error + assert.NoError(t, err) + assert.Equal(t, "value", plaintext) + }) + + t.Run("throws error on missing key", func(t *testing.T) { + // Run test + plaintext, err := decrypt("", value) + // Check error + assert.ErrorContains(t, err, "missing private key") + assert.Equal(t, value, plaintext) + }) + + t.Run("throws error on non-hex key", func(t *testing.T) { + // Run test + plaintext, err := decrypt("invalid", value) + // Check error + assert.ErrorContains(t, err, "failed to hex decode private key: cannot decode hex string") + assert.Equal(t, value, plaintext) + }) + + t.Run("throws error on non-base64 value", func(t *testing.T) { + // Run test + plaintext, err := decrypt(key, "encrypted:invalid") + // Check error + assert.ErrorContains(t, err, "failed to base64 decode secret: illegal base64 data at input byte 4") + assert.Equal(t, "encrypted:invalid", plaintext) + }) + + t.Run("throws error on empty ciphertext", func(t *testing.T) { + // Run test + plaintext, err := decrypt(key, "encrypted:") + // Check error + assert.ErrorContains(t, err, "failed to decrypt secret: invalid length of message") + assert.Equal(t, "encrypted:", plaintext) + }) +}