├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── decrypt.go ├── encrypt.go └── root.go ├── main.go └── secrets ├── aws.go ├── dev.go ├── dev_test.go ├── envelope.go ├── keyservice.go └── secrets.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 AgileBits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sm 2 | Simple secret management tool for server configuration 3 | 4 | [ ![Codeship Status for agilebits/sm](https://app.codeship.com/projects/33899e80-fae5-0134-b168-721cf569a862/status?branch=master)](https://app.codeship.com/projects/211385) 5 | 6 | ## How to build 7 | 8 | ``` 9 | go get -u -v github.com/agilebits/sm 10 | cd ~/go/src/github.com/agilebits/sm 11 | go install 12 | ``` 13 | ## Encrypt/decrypt data on development machines 14 | 15 | ``` 16 | cat app-config.yml | sm encrypt > app-config.sm 17 | cat app-config.sm | sm decrypt 18 | ``` 19 | 20 | On the first run, the utility will generate a new master key and store it in `~/.sm/masterkey` file. The `masterkey` must be saved and copied across all developer machines. 21 | 22 | 23 | ## Encrypt/decrypt data with Amazon Web Service KMS 24 | 25 | First, you have to create a master key using AWS IAM and give yourself permissions to use this key for encryption and decryption. 26 | 27 | ``` 28 | export AWS_REGION='us-east-1' 29 | export KMS_KEY_ID='arn:aws:kms:us-east-1:123123123123:key/d845cfa3-0719-4631-1d00-10ab63e40ddf' 30 | 31 | cat app-config.yml | sm encrypt \ 32 | --env aws \ 33 | --region $AWS_REGION \ 34 | --master $KMS_KEY_ID \ 35 | > app-config.sm 36 | 37 | cat app-config.sm | sm decrypt 38 | ``` 39 | 40 | ## Use jq to validate JSON files 41 | 42 | For example: 43 | ``` 44 | export AWS_REGION=us-east-1 45 | export KMS_KEY_ID=alias/YOUR-KEY-ALIAS 46 | 47 | jq --compact-output . < config.json | sm encrypt \ 48 | --env aws \ 49 | --region $AWS_REGION \ 50 | --master $KMS_KEY_ID \ 51 | > config.sm 52 | 53 | sm decrypt < config.sm | jq 54 | 55 | ``` 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /cmd/decrypt.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "github.com/agilebits/sm/secrets" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // decryptCmd represents the decrypt command 16 | var decryptCmd = &cobra.Command{ 17 | Use: "decrypt", 18 | Short: "Decrypt content using key management system", 19 | Long: `This command will decrypt content that was encrypted using encrypt command. 20 | 21 | It requires access to the same key management system (KMS) that was used for encryption. 22 | 23 | For example: 24 | 25 | cat encrypted-app-config.sm | sm decrypt > app-config.yml 26 | 27 | `, 28 | Run: func(cmd *cobra.Command, args []string) { 29 | reader := bufio.NewReader(os.Stdin) 30 | message, err := ioutil.ReadAll(reader) 31 | if err != nil { 32 | log.Fatal("failed to read:", err) 33 | } 34 | 35 | envelope := &secrets.Envelope{} 36 | if err := json.Unmarshal(message, &envelope); err != nil { 37 | log.Fatal("failed to Unmarshal:", err) 38 | } 39 | 40 | result, err := secrets.DecryptEnvelope(envelope) 41 | if err != nil { 42 | log.Fatal("failed to DecryptEnvelope:", err) 43 | } 44 | 45 | fmt.Println(string(result)) 46 | }, 47 | } 48 | 49 | func init() { 50 | RootCmd.AddCommand(decryptCmd) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/encrypt.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "github.com/agilebits/sm/secrets" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // encryptCmd represents the encrypt command 16 | var encryptCmd = &cobra.Command{ 17 | Use: "encrypt", 18 | Short: "Encrypt content using key management system", 19 | Long: ` 20 | 21 | Encrypt command is used to encrypt the contents of the standard input and write 22 | encrypted "envelope" into the standard output. 23 | 24 | The envelope is a JSON file that contains encrypted data along with the 25 | additional information that is needed to decrypt it back if the access to the 26 | key management system is available. 27 | 28 | For example: 29 | 30 | cat app-config.yml | sm encrypt --env aws --region us-east-1 --master arn:aws:kms:us-east-1:123123123123:key/d845cfa3-0719-4631-1d00-10ab63e40ddf > encrypted-app-config.sm 31 | `, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | reader := bufio.NewReader(os.Stdin) 34 | message, err := ioutil.ReadAll(reader) 35 | if err != nil { 36 | log.Fatal("failed to read:", err) 37 | } 38 | 39 | envelope, err := secrets.EncryptEnvelope(env, region, masterKeyID, message) 40 | if err != nil { 41 | log.Fatal("failed to encrypt:", err) 42 | } 43 | 44 | buf, err := json.Marshal(envelope) 45 | if err != nil { 46 | log.Fatal("failed to Marshal:", err) 47 | } 48 | 49 | fmt.Println(string(buf)) 50 | }, 51 | } 52 | 53 | func init() { 54 | RootCmd.AddCommand(encryptCmd) 55 | 56 | encryptCmd.Flags().StringVarP(&env, "env", "e", "dev", "Environment type: 'dev' or 'aws") 57 | encryptCmd.Flags().StringVarP(®ion, "region", "r", "", "AWS Region ('us-east-1')") 58 | encryptCmd.Flags().StringVarP(&masterKeyID, "master", "m", "", "Master key identifier") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | var cfgFile string 12 | 13 | // RootCmd represents the base command when called without any subcommands 14 | var RootCmd = &cobra.Command{ 15 | Use: "sm", 16 | Short: "Simple secret management tool", 17 | Long: ` 18 | Simple secret management tool used to protect secrets in the server apps. 19 | 20 | It relies on the key management system (KMS) provided by the server environment. 21 | For example, Amazon Web Services KMS is used for servers running on EC2 virtual 22 | machines. A fake KMS implementation can be used when running on developer machines 23 | to avoid storing unencrypted secrets in version control systems. 24 | 25 | sm can be used in a command-line to encrypt and decrypt configuration files. 26 | 27 | For example: 28 | 29 | cat plaintext.config.yaml | sm encrypt > encrypted.config.yaml 30 | `, 31 | // Uncomment the following line if your bare application 32 | // has an action associated with it: 33 | // Run: func(cmd *cobra.Command, args []string) { }, 34 | } 35 | 36 | // Execute adds all child commands to the root command sets flags appropriately. 37 | // This is called by main.main(). It only needs to happen once to the rootCmd. 38 | func Execute() { 39 | if err := RootCmd.Execute(); err != nil { 40 | fmt.Println(err) 41 | os.Exit(-1) 42 | } 43 | } 44 | 45 | var env string 46 | var region string 47 | var masterKeyID string 48 | 49 | func init() { 50 | cobra.OnInitialize(initConfig) 51 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sm/config.yaml)") 52 | } 53 | 54 | // initConfig reads in config file and ENV variables if set. 55 | func initConfig() { 56 | if cfgFile != "" { // enable ability to specify config file via flag 57 | viper.SetConfigFile(cfgFile) 58 | } 59 | 60 | viper.SetConfigName("config") // name of config file (without extension) 61 | viper.AddConfigPath("$HOME/.sm") // adding home directory as first search path 62 | viper.AutomaticEnv() // read in environment variables that match 63 | 64 | // If a config file is found, read it in. 65 | if err := viper.ReadInConfig(); err == nil { 66 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/agilebits/sm/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /secrets/aws.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/aws/aws-sdk-go/aws/ec2metadata" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/pkg/errors" 9 | 10 | "encoding/base64" 11 | 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/aws/credentials" 14 | "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" 15 | "github.com/aws/aws-sdk-go/service/kms" 16 | ) 17 | 18 | // AwsKeyService represents connection to Amazon Web Services KMS 19 | type AwsKeyService struct { 20 | lock sync.RWMutex 21 | 22 | region string 23 | masterKeyID string 24 | 25 | creds *credentials.Credentials 26 | service *kms.KMS 27 | } 28 | 29 | // NewAwsKeyService creates a new AwsKeyService in given AWS region and with the given masterKey identifier. 30 | func NewAwsKeyService(region string, masterKeyID string) *AwsKeyService { 31 | return &AwsKeyService{ 32 | region: region, 33 | masterKeyID: masterKeyID, 34 | } 35 | } 36 | 37 | func awsSession(region string) *session.Session { 38 | return session.New(&aws.Config{ 39 | Region: aws.String(region), 40 | }) 41 | } 42 | 43 | func (s *AwsKeyService) setup() error { 44 | s.lock.Lock() 45 | defer s.lock.Unlock() 46 | 47 | if s.service == nil || s.creds == nil || s.creds.IsExpired() { 48 | s.creds = credentials.NewChainCredentials( 49 | []credentials.Provider{ 50 | &credentials.EnvProvider{}, 51 | &credentials.SharedCredentialsProvider{}, 52 | &ec2rolecreds.EC2RoleProvider{ 53 | Client: ec2metadata.New(awsSession(s.region)), 54 | }, 55 | }) 56 | 57 | sess := session.New(&aws.Config{ 58 | Credentials: s.creds, 59 | Region: &s.region, 60 | }) 61 | 62 | s.service = kms.New(sess) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // GenerateKey generates a brand new ServerKey. 69 | func (s *AwsKeyService) GenerateKey(kid string) (*EncryptionKey, error) { 70 | if err := s.setup(); err != nil { 71 | return nil, errors.Wrapf(err, "failed to setup") 72 | } 73 | 74 | input := &kms.GenerateDataKeyInput{ 75 | EncryptionContext: map[string]*string{"kid": aws.String(kid)}, 76 | GrantTokens: []*string{aws.String("Encrypt"), aws.String("Decrypt")}, 77 | KeyId: aws.String(s.masterKeyID), 78 | KeySpec: aws.String("AES_256"), 79 | } 80 | 81 | out, err := s.service.GenerateDataKey(input) 82 | if err != nil { 83 | return nil, errors.Wrapf(err, "failed to GenerateDataKey") 84 | } 85 | 86 | result := &EncryptionKey{ 87 | KID: kid, 88 | Enc: A256GCM, 89 | RawKey: out.Plaintext, 90 | EncKey: base64.RawURLEncoding.EncodeToString(out.CiphertextBlob), 91 | } 92 | 93 | return result, nil 94 | } 95 | 96 | // DecryptKey decrypts an existing ServerKey. 97 | func (s *AwsKeyService) DecryptKey(key *EncryptionKey) error { 98 | if err := s.setup(); err != nil { 99 | return errors.Wrapf(err, "failed to setup") 100 | } 101 | 102 | ciphertextBlob, err := base64.RawURLEncoding.DecodeString(key.EncKey) 103 | if err != nil { 104 | return errors.Wrap(err, "failed to DecodeString") 105 | } 106 | 107 | input := &kms.DecryptInput{ 108 | CiphertextBlob: ciphertextBlob, 109 | EncryptionContext: map[string]*string{"kid": aws.String(key.KID)}, 110 | GrantTokens: []*string{aws.String("Encrypt"), aws.String("Decrypt")}, 111 | } 112 | 113 | out, err := s.service.Decrypt(input) 114 | if err != nil { 115 | return errors.Wrapf(err, "failed to Decrypt") 116 | } 117 | 118 | key.RawKey = out.Plaintext 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /secrets/dev.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "crypto/rand" 5 | "io/ioutil" 6 | "log" 7 | "os/user" 8 | "path" 9 | "strings" 10 | 11 | "encoding/json" 12 | 13 | "os" 14 | 15 | "encoding/base64" 16 | 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | // DevKeyService contains DevKeyService information 21 | type DevKeyService struct { 22 | masterKey *EncryptionKey 23 | } 24 | 25 | const masterKeyID = "master-key" 26 | 27 | // NewDevKeyService returns an empty DevKeyService object 28 | func NewDevKeyService() *DevKeyService { 29 | createDataDir() 30 | 31 | result := &DevKeyService{} 32 | masterKey, err := result.GenerateKey(masterKeyID) 33 | if err != nil { 34 | log.Fatal("NewDevKeyService failed to generate master key") 35 | } 36 | 37 | result.masterKey = masterKey 38 | return result 39 | } 40 | 41 | func dataDir() string { 42 | user, err := user.Current() 43 | if err != nil { 44 | log.Fatal("failed to obtain current user:", err) 45 | } 46 | 47 | return path.Join(user.HomeDir, ".sm") 48 | } 49 | 50 | func createDataDir() { 51 | if err := os.MkdirAll(dataDir(), 0700); err != nil { 52 | log.Fatal("failed to Mkdir:", err) 53 | } 54 | } 55 | 56 | func filepathForKeyID(kid string) string { 57 | filename := "" 58 | for _, ch := range strings.ToLower(kid) { 59 | if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') { 60 | filename = filename + string(ch) 61 | } 62 | } 63 | 64 | return path.Join(dataDir(), filename) 65 | } 66 | 67 | func readKey(kid string) (*EncryptionKey, error) { 68 | buf, err := ioutil.ReadFile(filepathForKeyID(kid)) 69 | if err != nil { 70 | return nil, errors.Wrapf(err, "readKey failed to ReadFile") 71 | } 72 | 73 | key := &EncryptionKey{} 74 | if err := json.Unmarshal(buf, &key); err != nil { 75 | return nil, errors.Wrapf(err, "readKey failed to Unmarshal") 76 | } 77 | 78 | return key, nil 79 | } 80 | 81 | func writeKey(key *EncryptionKey) error { 82 | buf, err := json.Marshal(key) 83 | if err != nil { 84 | return errors.Wrap(err, "writeKey failed to Marshal") 85 | } 86 | 87 | if err := ioutil.WriteFile(filepathForKeyID(key.KID), buf, 0700); err != nil { 88 | return errors.Wrap(err, "writeKey failed to WriteFile") 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // GenerateKey generates a new server key 95 | func (s *DevKeyService) GenerateKey(kid string) (*EncryptionKey, error) { 96 | result, err := readKey(kid) 97 | if err == nil { 98 | // key already exist, 99 | if err = s.DecryptKey(result); err != nil { 100 | return nil, errors.Wrap(err, "GenerateKey failed to DecryptKey") 101 | } 102 | return result, nil 103 | } 104 | 105 | rawKey := make([]byte, 32) 106 | if _, err := rand.Read(rawKey); err != nil { 107 | return nil, errors.Wrap(err, "GenerateKey failed to rand.Read") 108 | } 109 | 110 | var encKey string 111 | if kid == masterKeyID { 112 | // master key is stored in unencrypted 113 | encKey = base64.RawURLEncoding.EncodeToString(rawKey) 114 | } else { 115 | ciphertext, err := s.masterKey.Encrypt(rawKey) 116 | if err != nil { 117 | log.Fatal("GenerateKey failed to Encrypt with masterKey:", err) 118 | } 119 | encKey = base64.RawURLEncoding.EncodeToString(ciphertext) 120 | } 121 | 122 | result = &EncryptionKey{ 123 | KID: kid, 124 | Enc: A256GCM, 125 | EncKey: encKey, 126 | RawKey: rawKey, 127 | } 128 | 129 | if err := writeKey(result); err != nil { 130 | log.Fatal("GenerateKey failed to writeKey:", err) 131 | } 132 | 133 | return result, nil 134 | } 135 | 136 | // DecryptKey decrypts the dev key 137 | func (s *DevKeyService) DecryptKey(key *EncryptionKey) error { 138 | if key.RawKey != nil { 139 | return nil 140 | } 141 | 142 | encKey, err := base64.RawURLEncoding.DecodeString(key.EncKey) 143 | if err != nil { 144 | return errors.Wrap(err, "failed to decode base64url value") 145 | } 146 | 147 | if key.KID == masterKeyID { 148 | key.RawKey = encKey 149 | } else { 150 | plaintext, err := s.masterKey.Decrypt(encKey) 151 | if err != nil { 152 | return errors.Wrap(err, "failed to decrypt with master key") 153 | } 154 | key.RawKey = plaintext 155 | } 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /secrets/dev_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestCanGenerateKey(t *testing.T) { 9 | svc := NewDevKeyService() 10 | 11 | const kid = "key1" 12 | key1, err := svc.GenerateKey(kid) 13 | if err != nil { 14 | t.Fatal("failed to generate key1:", err) 15 | } 16 | 17 | key2, err := svc.GenerateKey(kid) 18 | if err != nil { 19 | t.Fatal("failed to generate key2:", err) 20 | } 21 | 22 | if key1.KID != kid { 23 | t.Errorf("expected key1.KID = %q, got %q", kid, key1.KID) 24 | } 25 | 26 | if key2.KID != kid { 27 | t.Errorf("expected key2.KID = %q, got %q", kid, key2.KID) 28 | } 29 | 30 | if bytes.Compare(key1.RawKey, key2.RawKey) != 0 { 31 | t.Errorf("expect the same RawKey in key1 and key2") 32 | } 33 | 34 | key3, err := svc.GenerateKey("key3") 35 | if err != nil { 36 | t.Fatal("failed to generate key3:", err) 37 | } 38 | 39 | if bytes.Compare(key1.RawKey, key3.RawKey) == 0 { 40 | t.Errorf("expect the different RawKey in key1 and key3") 41 | } 42 | } 43 | 44 | func TestCanEncryptAndDecrypt(t *testing.T) { 45 | svc := NewDevKeyService() 46 | 47 | key, err := svc.GenerateKey("somekey") 48 | if err != nil { 49 | t.Fatal("failed to generate key:", err) 50 | } 51 | 52 | source := "Hello, World!" 53 | ciphertext, err := key.Encrypt([]byte(source)) 54 | if err != nil { 55 | t.Fatal("failed to Encrypt:", err) 56 | } 57 | 58 | plaintext, err := key.Decrypt(ciphertext) 59 | if err != nil { 60 | t.Fatal("failed to Decrypt:", err) 61 | } 62 | 63 | if string(plaintext) != source { 64 | t.Errorf("encrypt/decrypt failed, expected %q but got %q", source, plaintext) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /secrets/envelope.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | // Envelope defines JSON structure that wraps the encrypted content 4 | type Envelope struct { 5 | Env string `json:"env"` 6 | Region string `json:"region,omitempty"` 7 | MasterKeyID string `json:"master,omitempty"` 8 | 9 | Key EncryptionKey `json:"key"` 10 | 11 | Data string `json:"data"` 12 | } 13 | -------------------------------------------------------------------------------- /secrets/keyservice.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // EncryptionKey contians server key information 15 | type EncryptionKey struct { 16 | KID string `json:"kid"` 17 | Enc string `json:"enc"` 18 | EncKey string `json:"encKey"` 19 | RawKey []byte `json:"-"` 20 | } 21 | 22 | // KeyService defines key methods 23 | type KeyService interface { 24 | GenerateKey(kid string) (*EncryptionKey, error) 25 | DecryptKey(key *EncryptionKey) error 26 | } 27 | 28 | // Ciphertext contains encrypted message 29 | type ciphertext struct { 30 | KID string `json:"kid"` 31 | Enc string `json:"enc"` 32 | Cty string `json:"cty"` 33 | Iv string `json:"iv"` 34 | Data string `json:"data"` 35 | } 36 | 37 | const ( 38 | // A256GCM identifies the encryption algorithm 39 | A256GCM = "A256GCM" 40 | 41 | // B5JWKJSON identifies content type 42 | B5JWKJSON = "b5+jwk+json" 43 | ) 44 | 45 | // Decrypt decrypts a given ciphertext byte array using the web crypto key 46 | func (key *EncryptionKey) Decrypt(message []byte) ([]byte, error) { 47 | m := &ciphertext{} 48 | if err := json.Unmarshal(message, &m); err != nil { 49 | var errorMsg string 50 | if len(message) < 200 { 51 | errorMsg = fmt.Sprintf("invalid JSON %+q", string(message)) 52 | } else { 53 | errorMsg = fmt.Sprintf("invalid JSON %+q...%+q", string(message[:120]), string(message[len(message)-75:])) 54 | } 55 | 56 | return nil, errors.Wrapf(err, errorMsg) 57 | } 58 | 59 | if m.KID != key.KID { 60 | return nil, fmt.Errorf("attempt to decrypt message with KID %v using different KID %v", m.KID, key.KID) 61 | } 62 | 63 | if m.Enc != A256GCM { 64 | return nil, fmt.Errorf("attempt to decrypt message with unknown enc: %+q", m.Enc) 65 | } 66 | 67 | if m.Cty != B5JWKJSON { 68 | return nil, fmt.Errorf("attempt to decrypt message with unknown cty: %+q", m.Cty) 69 | } 70 | 71 | ciphertext, err := base64.RawURLEncoding.DecodeString(m.Data) 72 | if err != nil { 73 | return nil, errors.Wrapf(err, "invalid ciphertext in the message") 74 | } 75 | 76 | block, err := aes.NewCipher(key.RawKey) 77 | if err != nil { 78 | return nil, errors.Wrapf(err, "failed to create NewCipher") 79 | } 80 | 81 | iv, err := base64.RawURLEncoding.DecodeString(m.Iv) 82 | if err != nil { 83 | return nil, errors.Wrapf(err, "invalid iv in the message") 84 | } 85 | 86 | if len(iv) != 12 { 87 | return nil, fmt.Errorf("invalid iv length (%d) in the message, expected 12", len(iv)) 88 | } 89 | 90 | aead, err := cipher.NewGCM(block) 91 | if err != nil { 92 | return nil, errors.Wrap(err, "failed to create NewGCM") 93 | } 94 | 95 | plaintext, err := aead.Open(nil, iv, ciphertext, nil) 96 | return plaintext, errors.Wrap(err, "failed to Open") 97 | } 98 | 99 | // Encrypt encrypts a given plaintext byte array 100 | func (key *EncryptionKey) Encrypt(plaintext []byte) ([]byte, error) { 101 | block, err := aes.NewCipher(key.RawKey) 102 | if err != nil { 103 | return nil, errors.Wrap(err, "failed to create NewCipher") 104 | } 105 | 106 | aead, err := cipher.NewGCM(block) 107 | if err != nil { 108 | return nil, errors.Wrap(err, "failed to create NewGCM") 109 | } 110 | 111 | iv := make([]byte, aead.NonceSize()) 112 | if _, err := rand.Read(iv); err != nil { 113 | return nil, errors.Wrap(err, "failed to get random iv") 114 | } 115 | 116 | data := aead.Seal(nil, iv, plaintext, nil) 117 | m := &ciphertext{ 118 | KID: key.KID, 119 | Enc: A256GCM, 120 | Cty: B5JWKJSON, 121 | Iv: base64.RawURLEncoding.EncodeToString(iv), 122 | Data: base64.RawURLEncoding.EncodeToString(data), 123 | } 124 | 125 | result, err := json.Marshal(m) 126 | return result, errors.Wrap(err, "failed to Marshal") 127 | } 128 | -------------------------------------------------------------------------------- /secrets/secrets.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const ( 12 | envDev = "dev" 13 | envAWS = "aws" 14 | ) 15 | 16 | // EncryptEnvelope will generate a new key and encrypt the message. It returns the Envelope that contains everything that is needed to decrypt the message (if the access to the KeyService is granted). 17 | func EncryptEnvelope(env, region, masterKeyID string, message []byte) (*Envelope, error) { 18 | keyService, err := getKeyService(env, region, masterKeyID) 19 | if err != nil { 20 | return nil, errors.Wrapf(err, "failed to obtain key service for env:%+q, region:%+q, masterKeyID:%+q", env, region, masterKeyID) 21 | } 22 | 23 | kid := "sm-" + time.Now().Format(time.RFC3339) 24 | encryptionKey, err := keyService.GenerateKey(kid) 25 | if err != nil { 26 | return nil, errors.Wrapf(err, "failed to generate encryption key") 27 | } 28 | 29 | ciphertext, err := encryptionKey.Encrypt(message) 30 | if err != nil { 31 | return nil, errors.Wrapf(err, "failed to encrypt") 32 | } 33 | 34 | envelope := &Envelope{ 35 | Env: env, 36 | Region: region, 37 | MasterKeyID: masterKeyID, 38 | Key: *encryptionKey, 39 | Data: base64.RawURLEncoding.EncodeToString(ciphertext), 40 | } 41 | 42 | return envelope, nil 43 | } 44 | 45 | // DecryptEnvelope will access the key service and decrypt the envelope. 46 | func DecryptEnvelope(envelope *Envelope) ([]byte, error) { 47 | keyService, err := getKeyService(envelope.Env, envelope.Region, envelope.MasterKeyID) 48 | if err != nil { 49 | return nil, errors.Wrapf(err, "failed to obtain key service for env:%+q, region:%+q, masterKeyID:%+q", envelope.Env, envelope.Region, envelope.MasterKeyID) 50 | } 51 | 52 | encryptionKey := &envelope.Key 53 | if err := keyService.DecryptKey(encryptionKey); err != nil { 54 | return nil, errors.Wrapf(err, "failed to decrypt key") 55 | } 56 | 57 | data, err := base64.RawURLEncoding.DecodeString(envelope.Data) 58 | if err != nil { 59 | return nil, errors.Wrapf(err, "failed to decode data") 60 | } 61 | 62 | result, err := encryptionKey.Decrypt(data) 63 | if err != nil { 64 | return nil, errors.Wrapf(err, "failed to decrypt data") 65 | } 66 | 67 | return result, nil 68 | } 69 | 70 | func getKeyService(env, region, masterKeyID string) (KeyService, error) { 71 | var keyService KeyService 72 | switch env { 73 | case envDev: 74 | keyService = NewDevKeyService() 75 | case envAWS: 76 | keyService = NewAwsKeyService(region, masterKeyID) 77 | default: 78 | return nil, fmt.Errorf("unsupported env: %+q", env) 79 | } 80 | 81 | return keyService, nil 82 | } 83 | --------------------------------------------------------------------------------