Skip to content

Commit

Permalink
exp/services/recoverysigner: add encrypt and decrypt commands (#2746)
Browse files Browse the repository at this point in the history
What
This PR adds two sub-commands encrypt and decrypt to encryption-tink-keyset to encrypt a cleartext keyset private or to decrypt an encrypted keyset private.

Why
I was planning on adding a sub-command reencrypt. However, I realized there's no way for someone who started the recovery signer with a cleartext keyset private without configuring encryption-kms-key-uri to encrypt the keyset later on, so I decided to add two sub-commands: encrypt and decrypt.

For someone who wants to make the system more secure by configuring the encryption-kms-key-uri at a later point in time, she can use the encrypt command to do so; for someone who wants to re-encrypt the keyset after rotating the KMS key, she can first use decrypt to get the cleartext keyset if no backup is available, then use encrypt to get a new encrypted keyset.
  • Loading branch information
howardtw authored Jun 26, 2020
1 parent 51656b3 commit d0a3c2c
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 25 deletions.
29 changes: 27 additions & 2 deletions exp/services/recoverysigner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ Usage:
recoverysigner [command]
Available Commands:
db Run database operations
serve Run the SEP-30 Recovery Signer server
db Run database operations
encryption-tink-keyset Run Tink keyset operations
serve Run the SEP-30 Recovery Signer server
Use "recoverysigner [command] --help" for more information about a command.
```
Expand Down Expand Up @@ -74,5 +75,29 @@ Flags:
Use "recoverysigner db [command] --help" for more information about a command.
```

## Usage: encryption-tink-keyset

The format of the --encryption-kms-key-uri configuration option should conform to the format stated in [Google Tink](https://github.com/google/tink/blob/040ac621b3e9ff7a240b1e596a423a30d32f9013/docs/KEY-MANAGEMENT.md#key-management-systems).
```
$ recoverysigner encryption-tink-keyset --help
Run Tink keyset operations
Usage:
recoverysigner encryption-tink-keyset [flags]
recoverysigner encryption-tink-keyset [command]
Available Commands:
create Create a new Tink keyset
decrypt Decrypt the Tink keyset specified in encryption-tink-keyset with the KMS key specified in encryption-kms-key-uri
encrypt Encrypt the Tink keyset specified in encryption-tink-keyset with the KMS key specified in encryption-kms-key-uri
rotate Rotate the Tink keyset specified in encryption-tink-keyset by generating a new key, adding it to the keyset, and making it the primary key in the keyset
Flags:
--encryption-kms-key-uri string URI for a remote KMS key used to encrypt Tink keyset (ENCRYPTION_KMS_KEY_URI)
--encryption-tink-keyset string Tink keyset to rotate/encrypt/decrypt (ENCRYPTION_TINK_KEYSET)
Use "recoverysigner encryption-tink-keyset [command] --help" for more information about a command.
```

[SEP-30]: https://github.com/stellar/stellar-protocol/blob/600c326b210d71ee031d7f3a40ca88191b4cdf9c/ecosystem/sep-0030.md
[README-Firebase.md]: README-Firebase.md
160 changes: 140 additions & 20 deletions exp/services/recoverysigner/cmd/keyset.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import (
)

type KeysetCommand struct {
Logger *supportlog.Entry
EncryptionKMSKeyURI string
EncryptionTinkKeyset string
Logger *supportlog.Entry
EncryptionKMSKeyURI string
EncryptionTinkKeysetJSON string
}

func (c *KeysetCommand) Command() *cobra.Command {
Expand All @@ -34,9 +34,9 @@ func (c *KeysetCommand) Command() *cobra.Command {
},
{
Name: "encryption-tink-keyset",
Usage: "Existing Tink keyset to rotate",
Usage: "Tink keyset to rotate/encrypt/decrypt",
OptType: types.String,
ConfigKey: &c.EncryptionTinkKeyset,
ConfigKey: &c.EncryptionTinkKeysetJSON,
FlagDefault: "",
Required: false,
},
Expand Down Expand Up @@ -67,9 +67,25 @@ func (c *KeysetCommand) Command() *cobra.Command {
c.Rotate()
},
}
decryptCmd := &cobra.Command{
Use: "decrypt",
Short: "Decrypt the Tink keyset specified in encryption-tink-keyset with the KMS key specified in encryption-kms-key-uri",
Run: func(_ *cobra.Command, _ []string) {
c.Decrypt()
},
}
encryptCmd := &cobra.Command{
Use: "encrypt",
Short: "Encrypt the Tink keyset specified in encryption-tink-keyset with the KMS key specified in encryption-kms-key-uri",
Run: func(_ *cobra.Command, _ []string) {
c.Encrypt()
},
}

cmd.AddCommand(createCmd)
cmd.AddCommand(rotateCmd)
cmd.AddCommand(decryptCmd)
cmd.AddCommand(encryptCmd)

return cmd
}
Expand Down Expand Up @@ -116,30 +132,30 @@ func createKeyset(kmsKeyURI string, keyTemplate *tinkpb.KeyTemplate) (publicClea

err = khPriv.Write(keyset.NewJSONWriter(&keysetPrivateEncrypted), aead)
if err != nil {
return "", "", "", errors.Wrap(err, "writing encrypted keyset containing private key")
return "", "", "", errors.Wrap(err, "writing encrypted keyset private")
}
}

err = insecurecleartextkeyset.Write(khPriv, keyset.NewJSONWriter(&keysetPrivateCleartext))
if err != nil {
return "", "", "", errors.Wrap(err, "writing cleartext keyset containing private key")
return "", "", "", errors.Wrap(err, "writing cleartext keyset private")
}

khPub, err := khPriv.Public()
if err != nil {
return "", "", "", errors.Wrap(err, "getting keyhandle for public key")
return "", "", "", errors.Wrap(err, "getting key handle for keyset public")
}

err = khPub.WriteWithNoSecrets(keyset.NewJSONWriter(&keysetPublic))
if err != nil {
return "", "", "", errors.Wrap(err, "writing cleartext keyset containing public key")
return "", "", "", errors.Wrap(err, "writing cleartext keyset public")
}

return keysetPublic.String(), keysetPrivateCleartext.String(), keysetPrivateEncrypted.String(), nil
}

func (c *KeysetCommand) Rotate() {
keysetPublic, keysetPrivateCleartext, keysetPrivateEncrypted, err := rotateKeyset(c.EncryptionKMSKeyURI, c.EncryptionTinkKeyset, c.keyTemplate())
keysetPublic, keysetPrivateCleartext, keysetPrivateEncrypted, err := rotateKeyset(c.EncryptionKMSKeyURI, c.EncryptionTinkKeysetJSON, c.keyTemplate())
if err != nil {
c.Logger.Errorf("Error rotating keyset: %v", err)
return
Expand All @@ -153,7 +169,7 @@ func (c *KeysetCommand) Rotate() {
}
}

func rotateKeyset(kmsKeyURI, currentTinkKeyset string, keyTemplate *tinkpb.KeyTemplate) (publicCleartext string, privateCleartext string, privateEncrypted string, err error) {
func rotateKeyset(kmsKeyURI, keysetJSON string, keyTemplate *tinkpb.KeyTemplate) (publicCleartext string, privateCleartext string, privateEncrypted string, err error) {
var (
khPriv *keyset.Handle
aead tink.AEAD
Expand All @@ -170,14 +186,14 @@ func rotateKeyset(kmsKeyURI, currentTinkKeyset string, keyTemplate *tinkpb.KeyTe
return "", "", "", errors.Wrap(kmsErr, "getting AEAD primitive from KMS")
}

khPriv, err = keyset.Read(keyset.NewJSONReader(strings.NewReader(currentTinkKeyset)), aead)
khPriv, err = keyset.Read(keyset.NewJSONReader(strings.NewReader(keysetJSON)), aead)
if err != nil {
return "", "", "", errors.Wrap(err, "reading encrypted keyset")
return "", "", "", errors.Wrap(err, "getting key handle for keyset private by reading an encrypted keyset")
}
} else {
khPriv, err = insecurecleartextkeyset.Read(keyset.NewJSONReader(strings.NewReader(currentTinkKeyset)))
khPriv, err = insecurecleartextkeyset.Read(keyset.NewJSONReader(strings.NewReader(keysetJSON)))
if err != nil {
return "", "", "", errors.Wrap(err, "getting key handle for private key")
return "", "", "", errors.Wrap(err, "getting key handle for keyset private by reading a cleartext keyset")
}
}

Expand All @@ -189,7 +205,7 @@ func rotateKeyset(kmsKeyURI, currentTinkKeyset string, keyTemplate *tinkpb.KeyTe

khPriv, err = m.Handle()
if err != nil {
return "", "", "", errors.Wrap(err, "creating handle for the new keyset")
return "", "", "", errors.Wrap(err, "creating key handle for the rotated keyset private")
}

keysetPrivateEncrypted := strings.Builder{}
Expand All @@ -199,24 +215,128 @@ func rotateKeyset(kmsKeyURI, currentTinkKeyset string, keyTemplate *tinkpb.KeyTe
if kmsKeyURI != "" {
err = khPriv.Write(keyset.NewJSONWriter(&keysetPrivateEncrypted), aead)
if err != nil {
return "", "", "", errors.Wrap(err, "writing encrypted keyset containing private keys")
return "", "", "", errors.Wrap(err, "writing encrypted keyset private")
}
}

err = insecurecleartextkeyset.Write(khPriv, keyset.NewJSONWriter(&keysetPrivateCleartext))
if err != nil {
return "", "", "", errors.Wrap(err, "writing cleartext keyset containing private keys")
return "", "", "", errors.Wrap(err, "writing cleartext keyset private")
}

khPub, err := khPriv.Public()
if err != nil {
return "", "", "", errors.Wrap(err, "getting keyhandle for public keys")
return "", "", "", errors.Wrap(err, "getting key handle for keyset public")
}

err = khPub.WriteWithNoSecrets(keyset.NewJSONWriter(&keysetPublic))
if err != nil {
return "", "", "", errors.Wrap(err, "writing cleartext keyset containing public keys")
return "", "", "", errors.Wrap(err, "writing cleartext keyset public")
}

return keysetPublic.String(), keysetPrivateCleartext.String(), keysetPrivateEncrypted.String(), nil
}

var errNoKMSKeyURI = errors.New("KMS Key URI is not configured")

func (c *KeysetCommand) Decrypt() {
keysetPublic, keysetPrivateCleartext, err := decryptKeyset(c.EncryptionKMSKeyURI, c.EncryptionTinkKeysetJSON)
if err != nil {
c.Logger.Errorf("Error decrypting keyset: %v", err)
return
}

c.Logger.Print("Cleartext keyset public:", keysetPublic)
c.Logger.Print("Cleartext keyset private:", keysetPrivateCleartext)
}

func decryptKeyset(kmsKeyURI, keysetJSON string) (publicCleartext string, privateCleartext string, err error) {
if kmsKeyURI == "" {
return "", "", errNoKMSKeyURI
}

kmsClient, err := awskms.NewClient(kmsKeyURI)
if err != nil {
return "", "", errors.Wrap(err, "initializing AWS KMS client")
}

aead, err := kmsClient.GetAEAD(kmsKeyURI)
if err != nil {
return "", "", errors.Wrap(err, "getting AEAD primitive from KMS")
}

khPriv, err := keyset.Read(keyset.NewJSONReader(strings.NewReader(keysetJSON)), aead)
if err != nil {
return "", "", errors.Wrap(err, "getting key handle for keyset private by reading an encrypted keyset")
}

keysetPrivateCleartext := strings.Builder{}
err = insecurecleartextkeyset.Write(khPriv, keyset.NewJSONWriter(&keysetPrivateCleartext))
if err != nil {
return "", "", errors.Wrap(err, "writing cleartext keyset private")
}

khPub, err := khPriv.Public()
if err != nil {
return "", "", errors.Wrap(err, "getting key handle for keyset public")
}

keysetPublic := strings.Builder{}
err = khPub.WriteWithNoSecrets(keyset.NewJSONWriter(&keysetPublic))
if err != nil {
return "", "", errors.Wrap(err, "writing cleartext keyset public")
}

return keysetPublic.String(), keysetPrivateCleartext.String(), nil
}

func (c *KeysetCommand) Encrypt() {
keysetPublic, keysetPrivateEncrypted, err := encryptKeyset(c.EncryptionKMSKeyURI, c.EncryptionTinkKeysetJSON)
if err != nil {
c.Logger.Errorf("Error encrypting keyset: %v", err)
return
}

c.Logger.Print("Cleartext keyset public:", keysetPublic)
c.Logger.Print("Encrypted keyset private:", keysetPrivateEncrypted)
}

func encryptKeyset(kmsKeyURI, keysetJSON string) (publicCleartext string, privateEncrypted string, err error) {
if kmsKeyURI == "" {
return "", "", errNoKMSKeyURI
}

kmsClient, err := awskms.NewClient(kmsKeyURI)
if err != nil {
return "", "", errors.Wrap(err, "initializing AWS KMS client")
}

aead, err := kmsClient.GetAEAD(kmsKeyURI)
if err != nil {
return "", "", errors.Wrap(err, "getting AEAD primitive from KMS")
}

khPriv, err := insecurecleartextkeyset.Read(keyset.NewJSONReader(strings.NewReader(keysetJSON)))
if err != nil {
return "", "", errors.Wrap(err, "getting key handle for keyset private by reading a cleartext keyset")
}

keysetPrivateEncrypted := strings.Builder{}
err = khPriv.Write(keyset.NewJSONWriter(&keysetPrivateEncrypted), aead)
if err != nil {
return "", "", errors.Wrap(err, "writing encrypted keyset private")
}

khPub, err := khPriv.Public()
if err != nil {
return "", "", errors.Wrap(err, "getting key handle for keyset public")
}

keysetPublic := strings.Builder{}
err = khPub.WriteWithNoSecrets(keyset.NewJSONWriter(&keysetPublic))
if err != nil {
return "", "", errors.Wrap(err, "writing cleartext keyset public")
}

return keysetPublic.String(), keysetPrivateEncrypted.String(), nil
}
26 changes: 24 additions & 2 deletions exp/services/recoverysigner/cmd/keyset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,30 @@ func TestRotateKeyset_invalidKMSKeyURI(t *testing.T) {
assert.Contains(t, err.Error(), "initializing AWS KMS client")
}

func TestRotateKeyset_noCurrentKeyset(t *testing.T) {
func TestRotateKeyset_noEncryptionTinkKeyset(t *testing.T) {
_, _, _, err := rotateKeyset("", "", hybrid.ECIESHKDFAES128GCMKeyTemplate())
require.Error(t, err)
assert.Contains(t, err.Error(), "getting key handle for private key")
assert.Contains(t, err.Error(), "getting key handle for keyset private by reading a cleartext keyset")
}

func TestDecryptKeyset_invalidKMSKeyURI(t *testing.T) {
// encrption-kms-key-uri is not configured
_, _, err := decryptKeyset("", "keysetJSON")
require.Error(t, err)
assert.Equal(t, err, errNoKMSKeyURI)

_, _, err = decryptKeyset("invalid-uri", "keysetJSON")
require.Error(t, err)
assert.Contains(t, err.Error(), "initializing AWS KMS client")
}

func TestEncryptKeyset_invalidKMSKeyURI(t *testing.T) {
// encrption-kms-key-uri is not configured
_, _, err := encryptKeyset("", "keysetJSON")
require.Error(t, err)
assert.Equal(t, err, errNoKMSKeyURI)

_, _, err = encryptKeyset("invalid-uri", "keysetJSON")
require.Error(t, err)
assert.Contains(t, err.Error(), "initializing AWS KMS client")
}
2 changes: 1 addition & 1 deletion exp/services/recoverysigner/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (c *ServeCommand) Command() *cobra.Command {
},
{
Name: "encryption-kms-key-uri",
Usage: "URI for a remote KMS key used to decrypt the Tink keyset provided in encryption-tink-keyset",
Usage: "URI for a remote KMS key used to decrypt the Tink keyset specified in encryption-tink-keyset",
OptType: types.String,
ConfigKey: &opts.EncryptionKMSKeyURI,
FlagDefault: "",
Expand Down

0 comments on commit d0a3c2c

Please sign in to comment.