mirror of
https://github.com/Luzifer/vault2env.git
synced 2024-12-20 12:11:19 +00:00
Rewrite obfuscator logic not to work line-based
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
ccc42063b4
commit
2fb748b87a
6 changed files with 217 additions and 70 deletions
5
go.mod
5
go.mod
|
@ -7,12 +7,13 @@ require (
|
||||||
github.com/Luzifer/rconfig/v2 v2.4.0
|
github.com/Luzifer/rconfig/v2 v2.4.0
|
||||||
github.com/hashicorp/vault/api v1.10.0
|
github.com/hashicorp/vault/api v1.10.0
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
@ -24,6 +25,7 @@ require (
|
||||||
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/crypto v0.16.0 // indirect
|
golang.org/x/crypto v0.16.0 // indirect
|
||||||
|
@ -33,4 +35,5 @@ require (
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -52,8 +52,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||||
|
|
55
main.go
55
main.go
|
@ -6,7 +6,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
@ -187,7 +186,21 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
obfuscate := prepareObfuscator(envData)
|
var (
|
||||||
|
stdoutObfuscate = newObfuscator(os.Stdout, envData, getReplaceFn(cfg.Obfuscate))
|
||||||
|
stderrObfuscate = newObfuscator(os.Stderr, envData, getReplaceFn(cfg.Obfuscate))
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := stderrObfuscate.Close(); err != nil {
|
||||||
|
logrus.WithError(err).Error("closing stderr")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
if err := stdoutObfuscate.Close(); err != nil {
|
||||||
|
logrus.WithError(err).Error("closing stdout")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
emap := env.ListToMap(os.Environ())
|
emap := env.ListToMap(os.Environ())
|
||||||
for k, v := range emap {
|
for k, v := range emap {
|
||||||
|
@ -200,40 +213,10 @@ func main() {
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Env = env.MapToList(envData)
|
cmd.Env = env.MapToList(envData)
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
cmd.Stderr = stderrObfuscate
|
||||||
if err != nil {
|
cmd.Stdout = stdoutObfuscate
|
||||||
logrus.WithError(err).Fatal("getting stderr pipe")
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
if err := cmd.Run(); err != nil {
|
||||||
if err != nil {
|
logrus.WithError(err).Fatal("running command")
|
||||||
logrus.WithError(err).Fatal("getting stdout pipe")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
logrus.WithError(err).Fatal("starting command")
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := new(sync.WaitGroup)
|
|
||||||
wg.Add(2) //nolint:gomnd
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if err := obfuscationTransport(stdout, os.Stdout, obfuscate); err != nil {
|
|
||||||
logrus.WithError(err).Error("obfuscating stdout")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if err := obfuscationTransport(stderr, os.Stderr, obfuscate); err != nil {
|
|
||||||
logrus.WithError(err).Error("obfuscating stderr")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
logrus.WithError(err).Fatal("error during command execution")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,28 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func prepareObfuscator(secrets map[string]string) func(string) string {
|
func replaceAsterisk(_, _ string) string { return "****" }
|
||||||
var prepare func(name, secret string) string
|
func replaceHash(_, secret string) string {
|
||||||
|
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(secret)))
|
||||||
|
}
|
||||||
|
func replaceName(name, _ string) string { return name }
|
||||||
|
|
||||||
switch cfg.Obfuscate {
|
func getReplaceFn(name string) replaceFn {
|
||||||
|
switch name {
|
||||||
case "asterisk":
|
case "asterisk":
|
||||||
prepare = func(name, secret string) string { return "****" }
|
return replaceAsterisk
|
||||||
|
|
||||||
case "hash":
|
case "hash":
|
||||||
prepare = func(name, secret string) string { return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(secret))) }
|
return replaceHash
|
||||||
|
|
||||||
case "name":
|
case "name":
|
||||||
prepare = func(name, secret string) string { return name }
|
return replaceName
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return func(in string) string { return in }
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
replacements := []string{}
|
|
||||||
|
|
||||||
for k, v := range secrets {
|
|
||||||
if k != "" && v != "" {
|
|
||||||
replacements = append(replacements, v, prepare(k, v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repl := strings.NewReplacer(replacements...)
|
|
||||||
|
|
||||||
return func(in string) string { return repl.Replace(in) }
|
|
||||||
}
|
|
||||||
|
|
||||||
func obfuscationTransport(in io.Reader, out io.Writer, obfuscate func(string) string) error {
|
|
||||||
s := bufio.NewScanner(in)
|
|
||||||
for s.Scan() {
|
|
||||||
fmt.Fprintln(out, obfuscate(s.Text()))
|
|
||||||
}
|
|
||||||
return errors.Wrapf(s.Err(), "Failed to scan in buffer")
|
|
||||||
}
|
}
|
||||||
|
|
112
obfuscator_writer.go
Normal file
112
obfuscator_writer.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
obfuscator struct {
|
||||||
|
buffer []byte
|
||||||
|
closed bool
|
||||||
|
longestSecretLen int
|
||||||
|
output io.Writer
|
||||||
|
secretReplacements [][2]string
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceFn func(name, secret string) string
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ io.WriteCloser = &obfuscator{}
|
||||||
|
|
||||||
|
func newObfuscator(output io.Writer, secrets map[string]string, fn replaceFn) *obfuscator {
|
||||||
|
// We're looking for the longest secret: That is half the amount of
|
||||||
|
// data we need to keep in the buffer in order to detect and replace
|
||||||
|
// the secrets before forwarding the data to the real writer
|
||||||
|
var longestSecretLen int
|
||||||
|
for _, s := range secrets {
|
||||||
|
if l := len(s); l > longestSecretLen {
|
||||||
|
longestSecretLen = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var replacements [][2]string
|
||||||
|
if fn == nil {
|
||||||
|
// Special case: No replacer is set, we can pass-through
|
||||||
|
longestSecretLen = 0
|
||||||
|
} else {
|
||||||
|
// Create a map of replacements
|
||||||
|
for name, secret := range secrets {
|
||||||
|
replacements = append(replacements, [2]string{secret, fn(name, secret)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(replacements, func(j, i int) bool {
|
||||||
|
return len(replacements[i][0]) < len(replacements[j][0])
|
||||||
|
})
|
||||||
|
|
||||||
|
return &obfuscator{
|
||||||
|
longestSecretLen: longestSecretLen,
|
||||||
|
output: output,
|
||||||
|
secretReplacements: replacements,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *obfuscator) Close() (err error) {
|
||||||
|
o.closed = true
|
||||||
|
|
||||||
|
// Do a last sweep on the remaining buffer
|
||||||
|
o.sanitizeBuffer()
|
||||||
|
|
||||||
|
// Copy the rest to the underlying writer
|
||||||
|
if _, err = o.output.Write(o.buffer); err != nil {
|
||||||
|
return fmt.Errorf("writing remaining buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *obfuscator) Write(data []byte) (n int, err error) {
|
||||||
|
if o.closed {
|
||||||
|
return 0, fmt.Errorf("write on closed writer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// First take everything from the input
|
||||||
|
o.buffer = append(o.buffer, data...)
|
||||||
|
|
||||||
|
// If we haven't enough data buffered lets just pretent we wrote
|
||||||
|
// everything and in reality do nothing
|
||||||
|
if len(o.buffer) < o.longestSecretLen*2 {
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we have at least twice the length of the longest secret in
|
||||||
|
// the buffer so we can sanitize the buffer…
|
||||||
|
o.sanitizeBuffer()
|
||||||
|
|
||||||
|
// Now that all secrets have been replaced, we can write everything
|
||||||
|
// to the writer except the last {longestSecretLen} bytes as they
|
||||||
|
// might contain a part of the longest secret
|
||||||
|
wrLen := len(o.buffer) - o.longestSecretLen
|
||||||
|
if wrLen < 1 {
|
||||||
|
// Nothing to write, buffer was shortened too much
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.Copy(o.output, bytes.NewReader(o.buffer[:wrLen])); err != nil {
|
||||||
|
return 0, fmt.Errorf("copying sanitized data to writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.buffer = o.buffer[wrLen:]
|
||||||
|
|
||||||
|
// We took everything from them: Lets tell them we wrote everything
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *obfuscator) sanitizeBuffer() {
|
||||||
|
for _, repl := range o.secretReplacements {
|
||||||
|
o.buffer = bytes.ReplaceAll(o.buffer, []byte(repl[0]), []byte(repl[1]))
|
||||||
|
}
|
||||||
|
}
|
71
obfuscator_writer_test.go
Normal file
71
obfuscator_writer_test.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObfuscation(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Input string
|
||||||
|
Expected string
|
||||||
|
ReplaceFn replaceFn
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Input: "this is a longer string with a secret embedded inside",
|
||||||
|
Expected: "this is a longer string with a **** embedded inside",
|
||||||
|
ReplaceFn: replaceAsterisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "this is very short",
|
||||||
|
Expected: "this is very short",
|
||||||
|
ReplaceFn: replaceAsterisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "secret",
|
||||||
|
Expected: "****",
|
||||||
|
ReplaceFn: replaceAsterisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "foo",
|
||||||
|
Expected: "foo",
|
||||||
|
ReplaceFn: replaceAsterisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "can we have this very long secret with some special #$% chars in it obfuscated?",
|
||||||
|
Expected: "can we have **** obfuscated?",
|
||||||
|
ReplaceFn: replaceAsterisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "secretsecret",
|
||||||
|
Expected: "********",
|
||||||
|
ReplaceFn: replaceAsterisk,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: "secret",
|
||||||
|
Expected: "secret",
|
||||||
|
ReplaceFn: nil, // Direct pass-through
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.Input, func(t *testing.T) {
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
obf := newObfuscator(out, map[string]string{
|
||||||
|
"mysecret": "secret",
|
||||||
|
"longsecret": "this very long secret with some special #$% chars in it",
|
||||||
|
}, c.ReplaceFn)
|
||||||
|
|
||||||
|
_, err := fmt.Fprint(obf, c.Input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, obf.Close())
|
||||||
|
|
||||||
|
assert.Equal(t, c.Expected, out.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue