diff --git a/exp/services/recoverysigner/README.md b/exp/services/recoverysigner/README.md index b3fbef1faa..ef849f8d7a 100644 --- a/exp/services/recoverysigner/README.md +++ b/exp/services/recoverysigner/README.md @@ -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. ``` @@ -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 diff --git a/exp/services/recoverysigner/cmd/keyset.go b/exp/services/recoverysigner/cmd/keyset.go index 61381f8cfa..37a638e844 100644 --- a/exp/services/recoverysigner/cmd/keyset.go +++ b/exp/services/recoverysigner/cmd/keyset.go @@ -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 { @@ -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, }, @@ -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 } @@ -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 @@ -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 @@ -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") } } @@ -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{} @@ -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 +} diff --git a/exp/services/recoverysigner/cmd/keyset_test.go b/exp/services/recoverysigner/cmd/keyset_test.go index f435e6cf4d..3777bf0e58 100644 --- a/exp/services/recoverysigner/cmd/keyset_test.go +++ b/exp/services/recoverysigner/cmd/keyset_test.go @@ -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") } diff --git a/exp/services/recoverysigner/cmd/serve.go b/exp/services/recoverysigner/cmd/serve.go index d624a1ee4a..b74147844f 100644 --- a/exp/services/recoverysigner/cmd/serve.go +++ b/exp/services/recoverysigner/cmd/serve.go @@ -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: "",