From 5431a19f7c5ae842e1b033b2ebcdb44823e5b30e Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Fri, 16 Jun 2017 15:34:40 +0200 Subject: [PATCH 01/10] Add exec command --- README.md | 25 +++---------- box.go | 23 ++++++++++++ commands.go | 29 +++++++++++++++ commands_test.go | 25 +++++++++++++ main.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8dcb3c8..649edf6 100644 --- a/README.md +++ b/README.md @@ -222,8 +222,9 @@ An runtime config automatically expanded by Lighter might look like ``` ## Container Startup Sequence -Docker images should embed the `secretary` executable. Call it at container startup to decrypt -environment variables, before starting the actual service. +Docker images should embed the `secretary` executable. Use it as an entrypoint +with `secretary exec --". It will then decrypt all command line arguments and +environment variables. *Dockerfile* ``` @@ -231,25 +232,9 @@ environment variables, before starting the actual service. ENV SECRETARY_VERSION x.y.z RUN curl -fsSLo /usr/bin/secretary "https://github.com/meltwater/secretary/releases/download/${SECRETARY_VERSION}/secretary-`uname -s`-`uname -m`" && \ chmod +x /usr/bin/secretary -``` - -Container startup examples -``` -#!/bin/sh -set -e - -# Decrypt secrets -if [ "$SERVICE_PUBLIC_KEY" != "" ]; then - SECRETS=$(secretary decrypt -e --service-key=/service/keys/service-private-key.pem) -else - SECRETS=$(secretary decrypt -e) -fi - -eval "$SECRETS" -unset SECRETS -# Start the service -exec ... +ENTRYPOINT ["/usr/bin/secretary", "exec", "--"] +CMD ["/bin/echo", "ENC[NACL,XXXXXX==]"] # This can be replaced in Dockerfiles that inherit from this one ``` The complete decryption sequence could be described as diff --git a/box.go b/box.go index f5ac618..33edd20 100644 --- a/box.go +++ b/box.go @@ -150,6 +150,29 @@ func genkey(publicKeyFile string, privateKeyFile string) { pemWrite(privateKey, privateKeyFile, "NACL PRIVATE KEY", 0600) } +func decryptEnvelopes(input string, decryptor DecryptionStrategy) (output string, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("%v", r) + } + } + }() + + repl := func(envelope string) string { + bytes, err := decryptor.Decrypt(envelope) + if err != nil { + panic(err) + } + return string(bytes) + } + + output = envelopeRegexp.ReplaceAllStringFunc(input, repl) + return +} + func extractEnvelopes(payload string) []string { return envelopeRegexp.FindAllString(payload, -1) } diff --git a/commands.go b/commands.go index b7c0be4..9b29f74 100644 --- a/commands.go +++ b/commands.go @@ -5,6 +5,7 @@ import ( "io" "io/ioutil" "os" + "path" "regexp" "strings" ) @@ -86,3 +87,31 @@ func decryptEnvironment(input []string, output io.Writer, crypto DecryptionStrat return ok, err } + +func createExecArgs(args []string, encryptedEnviron []string, crypto DecryptionStrategy) (cmd string, decryptedArgs []string, decryptedEnviron []string, err error) { + + cmd = args[0] + decryptedArgs = make([]string, len(args)) + + decryptedArgs[0] = path.Base(cmd) // By unix convention argv[0] has to be set to basename of command + for i, arg := range args[1:] { + decryptedArg, subErr := decryptEnvelopes(arg, crypto) + if subErr != nil { + err = fmt.Errorf("Error while decrypting argument: %v", subErr) + } + + decryptedArgs[i+1] = decryptedArg + } + + decryptedEnviron = make([]string, len(encryptedEnviron)) + for i, env := range encryptedEnviron { + decryptedEnv, subErr := decryptEnvelopes(env, crypto) + if subErr != nil { + err = fmt.Errorf("Error while decrypting environment variables: %v", subErr) + } + + decryptedEnviron[i] = decryptedEnv + } + + return +} diff --git a/commands_test.go b/commands_test.go index 318c61a..fe901c8 100644 --- a/commands_test.go +++ b/commands_test.go @@ -126,3 +126,28 @@ func TestDecryptEnvironmentCommandSubstringsSpaces(t *testing.T) { assert.Equal(t, "export b='blabla secretb la bla secret2'\n", output.String()) } + +func TestCreateExecArgs(t *testing.T) { + configPublicKey := pemRead("./resources/test/keys/config-public-key.pem") + configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem") + masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem") + masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem") + + encrypted, err := encryptEnvelope(masterPublicKey, configPrivateKey, []byte("Mellon")) + assert.Nil(t, err) + + encrypted2, err := encryptEnvelope(masterPublicKey, configPrivateKey, []byte("hunter2")) + assert.Nil(t, err) + + crypto := newKeyDecryptionStrategy(configPublicKey, masterPrivateKey) + + cmd, argv, environ, err := createExecArgs( + []string{"/bin/echo", encrypted}, + []string{"SECRET=" + encrypted2}, + crypto, + ) + assert.Nil(t, err) + assert.Equal(t, cmd, "/bin/echo") + assert.Equal(t, []string{"echo", "Mellon"}, argv) + assert.Equal(t, []string{"SECRET=hunter2"}, environ) +} diff --git a/main.go b/main.go index 272147d..ac5a79d 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + "syscall" + "github.com/go-errors/errors" "github.com/spf13/cobra" ) @@ -137,6 +139,96 @@ func main() { rootCmd.AddCommand(cmdDecrypt) } + // Exec command + { + var secretaryURL, appID, appVersion, taskID string + var configKeyFile, masterKeyFile, deployKeyFile, serviceKeyFile string + cmdExec := &cobra.Command{ + Use: "exec", + Short: "execute program, decrypting environment variables and arguments", + PreRun: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Print("Error: Must supply at least one command\n\n") + cmd.Usage() + os.Exit(1) + } + }, + Run: func(cmd *cobra.Command, args []string) { + var crypto DecryptionStrategy + + if len(secretaryURL) > 0 { + deployKey := requireKey("deploy private", deployKeyFile, "DEPLOY_PRIVATE_KEY", "./keys/master-private-key.pem") + masterKey := requireKey("master public", masterKeyFile, "MASTER_PUBLIC_KEY", "./keys/master-public-key.pem") + serviceKey := findKey(serviceKeyFile, "SERVICE_PRIVATE_KEY") + crypto = newDaemonDecryptionStrategy( + secretaryURL, appID, appVersion, taskID, + masterKey, deployKey, serviceKey) + } else { + // Send ENC[KMS,..] and ENC[NACL,...] to separate decryptors + composite := newCompositeDecryptionStrategy() + composite.Add("KMS", newKmsDecryptionStrategy(newKmsClient())) + + deployKey := findKey("deploy private", deployKeyFile, "DEPLOY_PRIVATE_KEY", "./keys/master-private-key.pem") + configKey := findKey("config public", configKeyFile, "CONFIG_PUBLIC_KEY", "./keys/config-public-key.pem") + if deployKey != nil && configKey != nil { + composite.Add("NACL", newKeyDecryptionStrategy(configKey, deployKey)) + } + + crypto = composite + } + + newCmd, newArgs, newEnvs, err := createExecArgs(args, os.Environ(), crypto) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(1) + } + + if err := syscall.Exec(newCmd, newArgs, newEnvs); err != nil { + fmt.Fprintf(os.Stderr, "Error when executing command \"%s\": %v\n", newCmd, err) + } + }, + } + + cmdExec.SetUsageTemplate(`Usage:{{if .Runnable}} + {{.UseLine}}{{if .HasFlags}} [flags]{{end}}{{end}}{{if .HasSubCommands}} + {{ .CommandPath}} [command]{{end}} -- cmd [args...]{{if gt .Aliases 0}} + +Aliases: + {{.NameAndAliases}} +{{end}}{{if .HasExample}} + +Examples: +{{ .Example }}{{end}}{{ if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if .IsAvailableCommand}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimRightSpace}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsHelpCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasSubCommands }} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +`) + + cmdExec.Flags().StringVarP(&deployKeyFile, "private-key", "", "", "Private key file for use without daemon") + + cmdExec.Flags().StringVarP(&configKeyFile, "config-key", "", "", "Config public key file") + cmdExec.Flags().StringVarP(&masterKeyFile, "master-key", "", "", "Master public key file") + cmdExec.Flags().StringVarP(&deployKeyFile, "deploy-key", "", "", "Private deploy key file") + cmdExec.Flags().StringVarP(&serviceKeyFile, "service-key", "", "", "Private service key file") + + cmdExec.Flags().StringVarP(&secretaryURL, "secretary-url", "s", os.Getenv("SECRETARY_URL"), "URL of secretary daemon, e.g. https://secretary:5070") + cmdExec.Flags().StringVarP(&appID, "app-id", "", os.Getenv("MARATHON_APP_ID"), "Marathon app id") + cmdExec.Flags().StringVarP(&appVersion, "app-version", "", os.Getenv("MARATHON_APP_VERSION"), "Marathon app config version") + cmdExec.Flags().StringVarP(&taskID, "task-id", "", os.Getenv("MESOS_TASK_ID"), "Mesos task id") + rootCmd.AddCommand(cmdExec) + } + // Daemon command { var marathonURL, configKeyFile, masterKeyFile, tlsCertFile, tlsKeyFile, daemonIP string From 8f89597b2af2f4f3318663610738dfdbc6e5683f Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 08:19:05 +0200 Subject: [PATCH 02/10] Add more testcases for exec command --- commands_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/commands_test.go b/commands_test.go index fe901c8..4deece8 100644 --- a/commands_test.go +++ b/commands_test.go @@ -151,3 +151,60 @@ func TestCreateExecArgs(t *testing.T) { assert.Equal(t, []string{"echo", "Mellon"}, argv) assert.Equal(t, []string{"SECRET=hunter2"}, environ) } + +func TestCreateExecArgsInvalidEnvelope(t *testing.T) { + configPublicKey := pemRead("./resources/test/keys/config-public-key.pem") + // configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem") + // masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem") + masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem") + + crypto := newKeyDecryptionStrategy(configPublicKey, masterPrivateKey) + + _, _, _, err := createExecArgs( + []string{"/bin/echo", "ENC[NACL,invalidenvelope]"}, + []string{}, + crypto, + ) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Error while decrypting argument") +} + +func TestCreateExecArgsInvalidEnvelopeInEnvironment(t *testing.T) { + configPublicKey := pemRead("./resources/test/keys/config-public-key.pem") + // configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem") + // masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem") + masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem") + + crypto := newKeyDecryptionStrategy(configPublicKey, masterPrivateKey) + + _, _, _, err := createExecArgs( + []string{"/bin/echo"}, + []string{"ENC[NACL,invalidenvelope]"}, + crypto, + ) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Error while decrypting environment") +} + +func TestCreateExecArgsInvalidDecryptionKey(t *testing.T) { + configPublicKey := pemRead("./resources/test/keys/config-public-key.pem") + configPrivateKey := pemRead("./resources/test/keys/config-private-key.pem") + masterPublicKey := pemRead("./resources/test/keys/master-public-key.pem") + //masterPrivateKey := pemRead("./resources/test/keys/master-private-key.pem") + + encrypted, err := encryptEnvelope(masterPublicKey, configPrivateKey, []byte("Mellon")) + assert.Nil(t, err) + + // NB: Erroneously using configPrivateKey here + crypto := newKeyDecryptionStrategy(configPublicKey, configPrivateKey) + + _, _, _, err = createExecArgs( + []string{"/bin/echo", encrypted}, + []string{}, + crypto, + ) + + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Error while decrypting argument") + assert.Contains(t, err.Error(), "incorrect keys?") +} From f45e3d6b66d943080dec81d3f086af333bb6853e Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 11:17:31 +0200 Subject: [PATCH 03/10] Add comment for the SetUsageTemplate in exec command --- main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.go b/main.go index ac5a79d..da6dc1b 100644 --- a/main.go +++ b/main.go @@ -189,6 +189,9 @@ func main() { }, } + // The usage template is here in order to give the usage text: + // secretary exec [flags] -- cmd [args...] + // No immediately obvious way to accomplish that other than replacing the entire usage template. cmdExec.SetUsageTemplate(`Usage:{{if .Runnable}} {{.UseLine}}{{if .HasFlags}} [flags]{{end}}{{end}}{{if .HasSubCommands}} {{ .CommandPath}} [command]{{end}} -- cmd [args...]{{if gt .Aliases 0}} From 9c94e38cf920ceb6b2773d0c7426265bb56e8942 Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 11:33:03 +0200 Subject: [PATCH 04/10] Remove extractEnvelopes function --- box.go | 4 ---- box_test.go | 36 +++++++++--------------------------- commands.go | 40 +++++++++++----------------------------- 3 files changed, 20 insertions(+), 60 deletions(-) diff --git a/box.go b/box.go index 33edd20..fe8d49d 100644 --- a/box.go +++ b/box.go @@ -173,10 +173,6 @@ func decryptEnvelopes(input string, decryptor DecryptionStrategy) (output string return } -func extractEnvelopes(payload string) []string { - return envelopeRegexp.FindAllString(payload, -1) -} - func extractEnvelopeType(envelope string) string { submatches := envelopeRegexp.FindStringSubmatch(envelope) if submatches != nil { diff --git a/box_test.go b/box_test.go index b4d54e7..d03137b 100644 --- a/box_test.go +++ b/box_test.go @@ -47,32 +47,6 @@ func TestFindKey(t *testing.T) { assert.Nil(t, findKey("", "RANDOM_ENVVAR_THAT_DOESNT_EXIST", "./resources/test/keys/nonexist-public-key.pem")) } -func TestExtractEnvelopes(t *testing.T) { - envelopes := extractEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/") - assert.Equal(t, 2, len(envelopes)) - assert.Equal(t, []string{"ENC[NACL,uSr123+/=]", "ENC[NACL,pWd123+/=]"}, envelopes) - - envelopes = extractEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/ENC[NACL,def123+/=]") - assert.Equal(t, 3, len(envelopes)) - assert.Equal(t, []string{"ENC[NACL,uSr123+/=]", "ENC[NACL,pWd123+/=]", "ENC[NACL,def123+/=]"}, envelopes) - - envelopes = extractEnvelopes("amqp://ENC[NACL,]:ENC[NACL,pWd123+/=]@rabbit:5672/") - assert.Equal(t, 1, len(envelopes)) - assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes) - - envelopes = extractEnvelopes("amqp://ENC[NACL,:ENC[NACL,pWd123+/=]@rabbit:5672/") - assert.Equal(t, 1, len(envelopes)) - assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes) - - envelopes = extractEnvelopes("amqp://NC[NACL,]:ENC[NACL,pWd123+/=]@rabbit:5672/") - assert.Equal(t, 1, len(envelopes)) - assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes) - - envelopes = extractEnvelopes("amqp://ENC[NACL,abc:ENC[NACL,pWd123+/=]@rabbit:5672/") - assert.Equal(t, 1, len(envelopes)) - assert.Equal(t, []string{"ENC[NACL,pWd123+/=]"}, envelopes) -} - func TestExtractEnvelopeType(t *testing.T) { assert.Equal(t, "", extractEnvelopeType("ENC[NACL,]")) assert.Equal(t, "NACL", extractEnvelopeType("ENC[NACL,abc]")) @@ -134,8 +108,16 @@ func TestEncryptEnvelope(t *testing.T) { assert.Equal(t, "secret", string(plaintext), "Should decrypt plaintext") } +type noopDecryptionStrategyType struct{} + +func (noopDecryptionStrategyType) Decrypt(envelope string) ([]byte, error) { + return []byte(envelope), nil +} + +var NoopDecryptionStrategy DecryptionStrategy = noopDecryptionStrategyType{} + func BenchmarkExtractEnvelopes(b *testing.B) { for n := 0; n < b.N; n++ { - extractEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/") + decryptEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/", NoopDecryptionStrategy) } } diff --git a/commands.go b/commands.go index 9b29f74..def51c8 100644 --- a/commands.go +++ b/commands.go @@ -36,18 +36,8 @@ func encryptCommand(input io.Reader, output io.Writer, crypto EncryptionStrategy func decryptStream(input io.Reader, output io.Writer, crypto DecryptionStrategy) { payload, err := ioutil.ReadAll(input) check(err, "Failed to read encrypted data from standard input") - result := string(payload) - - envelopes := extractEnvelopes(string(payload)) - if len(envelopes) > 0 { - for _, envelope := range envelopes { - plaintext, err := crypto.Decrypt(stripWhitespace(envelope)) - check(err) - - result = strings.Replace(result, envelope, string(plaintext), 1) - } - } - + result, err := decryptEnvelopes(string(payload), crypto) + check(err, "Failed to decrypt from standard input") output.Write([]byte(result)) } @@ -60,28 +50,20 @@ func decryptEnvironment(input []string, output io.Writer, crypto DecryptionStrat keyval := strings.SplitN(item, "=", 2) key, value := keyval[0], keyval[1] result := value - - envelopes := extractEnvelopes(value) - if len(envelopes) > 0 { + decryptedResult, suberr := decryptEnvelopes(result, crypto) + if suberr != nil { + ok = false + err = suberr + fmt.Fprintf(os.Stderr, "%s: %s\n", key, err) + continue + } + if decryptedResult != result { if !shellIdentifierRegexp.Match([]byte(key)) { ok = false err = fmt.Errorf("the env var '%s' is not a valid shell script identifier. Only alphanumeric characters and underscores are supported, starting with an alphabetic or underscore character", key) fmt.Fprintf(os.Stderr, "%s: %s\n", key, err) } - - for _, envelope := range envelopes { - plaintext, suberr := crypto.Decrypt(stripWhitespace(envelope)) - if suberr != nil { - ok = false - err = suberr - fmt.Fprintf(os.Stderr, "%s: %s\n", key, err) - continue - } - - result = strings.Replace(result, envelope, string(plaintext), 1) - } - - fmt.Fprintf(output, "export %s='%s'\n", key, result) + fmt.Fprintf(output, "export %s='%s'\n", key, decryptedResult) } } From cfbe0f8cb1c90ddebd804b72cb697bdb054c43fa Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 11:34:30 +0200 Subject: [PATCH 05/10] Rename variable in decryptEnvelopes --- box.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/box.go b/box.go index fe8d49d..0d389ac 100644 --- a/box.go +++ b/box.go @@ -150,7 +150,7 @@ func genkey(publicKeyFile string, privateKeyFile string) { pemWrite(privateKey, privateKeyFile, "NACL PRIVATE KEY", 0600) } -func decryptEnvelopes(input string, decryptor DecryptionStrategy) (output string, err error) { +func decryptEnvelopes(input string, crypto DecryptionStrategy) (output string, err error) { defer func() { if r := recover(); r != nil { var ok bool @@ -162,7 +162,7 @@ func decryptEnvelopes(input string, decryptor DecryptionStrategy) (output string }() repl := func(envelope string) string { - bytes, err := decryptor.Decrypt(envelope) + bytes, err := crypto.Decrypt(envelope) if err != nil { panic(err) } From 734d66df89ee824f79bc1f4cc7f3b318b65a3720 Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 17:37:54 +0200 Subject: [PATCH 06/10] Rename BenchmarkExtractEnvelopes to BenchmarkDecryptEnvelopes --- box_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/box_test.go b/box_test.go index d03137b..c3d046d 100644 --- a/box_test.go +++ b/box_test.go @@ -116,7 +116,7 @@ func (noopDecryptionStrategyType) Decrypt(envelope string) ([]byte, error) { var NoopDecryptionStrategy DecryptionStrategy = noopDecryptionStrategyType{} -func BenchmarkExtractEnvelopes(b *testing.B) { +func BenchmarkDecryptEnvelopes(b *testing.B) { for n := 0; n < b.N; n++ { decryptEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/", NoopDecryptionStrategy) } From cce87e44b9b1c84d82d45ab8f3039527a9dd23e1 Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 17:38:42 +0200 Subject: [PATCH 07/10] Add test for decryptEnvelopes --- box_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/box_test.go b/box_test.go index c3d046d..84a83f8 100644 --- a/box_test.go +++ b/box_test.go @@ -121,3 +121,18 @@ func BenchmarkDecryptEnvelopes(b *testing.B) { decryptEnvelopes("amqp://ENC[NACL,uSr123+/=]:ENC[NACL,pWd123+/=]@rabbit:5672/", NoopDecryptionStrategy) } } + +func TestDecryptEnvelopes(t *testing.T) { + privkey, err := pemDecode(privateKey) + assert.Nil(t, err) + + pubkey, err := pemDecode(publicKey) + assert.Nil(t, err) + + envelope, err := encryptEnvelope(pubkey, privkey, []byte("secret")) + crypto := newKeyDecryptionStrategy(pubkey, privkey) + + result, err := decryptEnvelopes("This is a "+envelope+" message", crypto) + assert.Nil(t, err) + assert.Equal(t, "This is a secret message", result) +} From 49f3706f6ea8711ac74377d9af9fd0c7df12f868 Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Mon, 19 Jun 2017 17:39:08 +0200 Subject: [PATCH 08/10] decryptEnvelopes strips whitespace --- box.go | 2 +- box_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/box.go b/box.go index 0d389ac..60dad2e 100644 --- a/box.go +++ b/box.go @@ -162,7 +162,7 @@ func decryptEnvelopes(input string, crypto DecryptionStrategy) (output string, e }() repl := func(envelope string) string { - bytes, err := crypto.Decrypt(envelope) + bytes, err := crypto.Decrypt(stripWhitespace(envelope)) if err != nil { panic(err) } diff --git a/box_test.go b/box_test.go index 84a83f8..d9856b4 100644 --- a/box_test.go +++ b/box_test.go @@ -136,3 +136,22 @@ func TestDecryptEnvelopes(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "This is a secret message", result) } + +func TestDecryptEnvelopesStripsWhitespace(t *testing.T) { + privkey, err := pemDecode(privateKey) + assert.Nil(t, err) + + pubkey, err := pemDecode(publicKey) + assert.Nil(t, err) + + crypto := newKeyDecryptionStrategy(pubkey, privkey) + + envelope, err := encryptEnvelope(pubkey, privkey, []byte("secret")) + + // Add some whitespace to the middle of the envelope + envelope = envelope[0:len(envelope)/2] + " \t \n \t " + envelope[len(envelope)/2:len(envelope)] + + result, err := decryptEnvelopes("This is a "+envelope+" message", crypto) + assert.Nil(t, err, "%v", err) + assert.Equal(t, "This is a secret message", result) +} From 0fd59c18d3d5716ae65355d783df502c0bd4cf66 Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Wed, 21 Jun 2017 09:27:51 +0200 Subject: [PATCH 09/10] Support passing path to keys in environment variables --- box.go | 14 +++++++++++--- box_test.go | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/box.go b/box.go index 60dad2e..1de8ff9 100644 --- a/box.go +++ b/box.go @@ -86,13 +86,21 @@ func findKey(locations ...string) *[32]byte { continue } - encoded := os.Getenv(location) - if encoded != "" { - key, err := pemDecode(encoded) + // First try to interpret the argument as an environment variable + env := os.Getenv(location) + if env != "" { + // If the environment variable is the path to an existing file, use that + if _, err := os.Stat(env); err == nil { + return pemRead(env) + } + + // Otherwise, try to decode the key as a pem-formatted string + key, err := pemDecode(env) check(err, "Failed to decode key in $%s", location) return key } + // If there is no such environment variable, check whether it is a path to a file if _, err := os.Stat(location); err == nil { return pemRead(location) } diff --git a/box_test.go b/box_test.go index d9856b4..f41e5b2 100644 --- a/box_test.go +++ b/box_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/crypto/nacl/box" + "io/ioutil" + "os" ) const privateKey = `Q1PuWtB1E7F1sLpvfBGjL+ZuH+fSCOvMDqTyRQE4GTg=` @@ -47,6 +49,19 @@ func TestFindKey(t *testing.T) { assert.Nil(t, findKey("", "RANDOM_ENVVAR_THAT_DOESNT_EXIST", "./resources/test/keys/nonexist-public-key.pem")) } +func TestFindKeyEnvironmentPem(t *testing.T) { + bytes, _ := ioutil.ReadFile("./resources/test/keys/config-public-key.pem") + os.Setenv("PUBLIC_KEY", string(bytes)) + expected, _ := pemDecode(string(bytes)) + assert.Equal(t, expected, findKey("PUBLIC_KEY")) +} + +func TestFindKeyEnvironmentPath(t *testing.T) { + os.Setenv("PUBLIC_KEY", "./resources/test/keys/config-public-key.pem") + expected := pemRead("./resources/test/keys/config-public-key.pem") + assert.Equal(t, expected, findKey("PUBLIC_KEY")) +} + func TestExtractEnvelopeType(t *testing.T) { assert.Equal(t, "", extractEnvelopeType("ENC[NACL,]")) assert.Equal(t, "NACL", extractEnvelopeType("ENC[NACL,abc]")) From 495365c2eb14f3a3890e032fd99158e27e965fb0 Mon Sep 17 00:00:00 2001 From: Petter Remen Date: Wed, 21 Jun 2017 09:33:34 +0200 Subject: [PATCH 10/10] Add documentation about setting SERVICE_PRIVATE_KEY --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 649edf6..3196dab 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ variables: override: env: SERVICE_PUBLIC_KEY: "%{secretary.service.publickey}" + SERVICE_PRIVATE_KEY: /service/keys/service-private-key.pem container: volumes: - containerPath: "/service/keys" @@ -212,6 +213,7 @@ An runtime config automatically expanded by Lighter might look like "DEPLOY_PUBLIC_KEY": "0k+v11LV3SOr+XiFJ/ug0KcPPhwkXnVirmO65nAd1LI=", "DEPLOY_PRIVATE_KEY": "rEmz7Rt6tUnlC4TKYeNzePYg+p1ePAw4BAtfJAY4zzs=", "SERVICE_PUBLIC_KEY": "/1fbWGMTaR+lLQJnEsmxdfwWybKOpPQpyWB3FpNmOF4=", + "SERVICE_PRIVATE_KEY": "/service/keys/service-private-key.pem" "DATABASE_USERNAME": "myservice", "DATABASE_PASSWORD": "ENC[NACL,SLXf+O9iG48uyojT0Zg30Q8/uRV8DizuDWMWtgL5PmTU54jxp5cTGrYeLpd86rA=]", "DATABASE_URL": "jdbc:mysql://hostname:3306/schema"