Repository: google/exposure-notifications-server
Pull request #740 (2020-07-17 23:47:20)

Add encrypt/decrypt operations to key management interface
* add implementation in all supported key management services
  * AEAD is missing from azure keyvault, have reached out for help
* In support of #734

google-oss-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/740#" title="Author self-approved">mikehelmick</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/main/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
mikehelmick: rebased on main
sethvargo: /lgtm
/approve
google-oss-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/740#" title="Author self-approved">mikehelmick</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/740#issuecomment-660513151" title="Approved">sethvargo</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/main/OWNERS)~~ [mikehelmick,sethvargo]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/go.sum b/go.sum
index b4d0b84..06477e5 100644
--- a/go.sum
+++ b/go.sum
@@ -1532,13 +1532,10 @@ google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb
 google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40=
 google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
 google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0 h1:cG03eaksBzhfSIk7JRGctfp3lanklcOM/mTGvow7BbQ=
 google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.25.0 h1:LodzhlzZEUfhXzNUMIfVlf9Gr6Ua5MMtoFWh7+f47qA=
 google.golang.org/api v0.25.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk=
@@ -1550,7 +1547,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@@ -1585,12 +1581,9 @@ google.golang.org/genproto v0.0.0-20200409111301-baae70f3302d/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200416231807-8751e049a2a0/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 h1:fiNLklpBwWK1mth30Hlwk+fcdBmIALlgF5iy77O37Ig=
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece h1:1YM0uhfumvoDu9sx8+RyWwTI63zoCQvI23IYFRlvte0=
 google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -1614,7 +1607,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
 google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE=
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
@@ -1622,14 +1614,10 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY=
 google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
@@ -1639,7 +1627,6 @@ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzyc
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1651,7 +1638,6 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a
 gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y=
 gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -1664,10 +1650,8 @@ gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3M
 gopkg.in/ory-am/dockertest.v3 v3.3.4/go.mod h1:s9mmoLkaGeAh97qygnNj4xWkiN7e1SKekYC6CovU+ek=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/square/go-jose.v2 v2.4.1 h1:H0TmLt7/KmzlrDOpa1F+zr0Tk90PbJYBfsVUmRLrf9Y=
 gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
 gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@@ -1692,7 +1676,6 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
diff --git a/pkg/api/v1alpha1/exposure_types.go b/pkg/api/v1alpha1/exposure_types.go
index 85703f0..18be584 100644
--- a/pkg/api/v1alpha1/exposure_types.go
+++ b/pkg/api/v1alpha1/exposure_types.go
@@ -56,6 +56,10 @@ const (
 //  - Does not have to be within range of any of the provided keys (i.e. future
 //    key uploads)
 //
+// revisionToken: An opaque string that must be passed in-tact from on additional
+//   publish requests from the same device, there the same TEKs may be published
+//   again.
+//
 // Padding: random base64 encoded data to obscure the request size.
 //
 // The following fields are deprecated, but accepted for backwards-compatibility:
@@ -68,6 +72,7 @@ type Publish struct {
 	VerificationPayload  string        `json:"verificationPayload"`
 	HMACKey              string        `json:"hmackey"`
 	SymptomOnsetInterval int32         `json:"symptomOnsetInterval"`
+	RevisionToken        string        `json:"revisionToken"`
 
 	Padding string `json:"padding"`
 
@@ -75,6 +80,16 @@ type Publish struct {
 	DeviceVerificationPayload string `json:"deviceVerificationPayload"` // DEPRECATED
 }
 
+// PublishResponse is sent back to the client on a publish request.
+// If successful, the revisionToken indicates an opaque string that must be
+// passed back if the same devices wishes to publish TEKs again.
+//
+// On error, the error field will contain the error details.
+type PublishResponse struct {
+	RevisionToken string `json:"revisionToken"`
+	Error         string `json:"error"`
+}
+
 // ExposureKey is the 16 byte key, the start time of the key and the
 // duration of the key. A duration of 0 means 24 hours.
 // - ALL fields are REQUIRED and must meet the constraints below.
diff --git a/pkg/keys/aws_kms.go b/pkg/keys/aws_kms.go
index f54efc9..e6a5f92 100644
--- a/pkg/keys/aws_kms.go
+++ b/pkg/keys/aws_kms.go
@@ -17,8 +17,10 @@ package keys
 import (
 	"context"
 	"crypto"
+	"encoding/base64"
 	"fmt"
 
+	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/kms"
 	"github.com/lstoll/awskms"
@@ -49,3 +51,33 @@ func NewAWSKMS(ctx context.Context) (KeyManager, error) {
 func (s *AWSKMS) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
 	return awskms.NewSigner(ctx, s.svc, keyID)
 }
+
+func (s *AWSKMS) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) {
+	input := kms.EncryptInput{
+		KeyId: &keyID,
+		EncryptionContext: map[string]*string{
+			"aad": aws.String(base64.StdEncoding.EncodeToString(aad)),
+		},
+		Plaintext: plaintext,
+	}
+	output, err := s.svc.EncryptWithContext(ctx, &input)
+	if err != nil {
+		return nil, fmt.Errorf("failed to encrypt: %w", err)
+	}
+	return output.CiphertextBlob, nil
+}
+
+func (s *AWSKMS) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) {
+	input := kms.DecryptInput{
+		KeyId: &keyID,
+		EncryptionContext: map[string]*string{
+			"aad": aws.String(base64.StdEncoding.EncodeToString(aad)),
+		},
+		CiphertextBlob: ciphertext,
+	}
+	output, err := s.svc.DecryptWithContext(ctx, &input)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decrypt: %w", err)
+	}
+	return output.Plaintext, nil
+}
diff --git a/pkg/keys/azure_keyvault.go b/pkg/keys/azure_keyvault.go
index fec18f6..2ec57f0 100644
--- a/pkg/keys/azure_keyvault.go
+++ b/pkg/keys/azure_keyvault.go
@@ -42,6 +42,24 @@ type AzureKeyVault struct {
 	client *keyvault.BaseClient
 }
 
+type AZKeyID struct {
+	Vault   string
+	Key     string
+	Version string
+}
+
+func ParseAZKeyID(keyID string) (*AZKeyID, error) {
+	parts := strings.SplitN(keyID, "/", 3)
+	if len(parts) < 3 {
+		return nil, fmt.Errorf("key must include vaultName, keyName, and keyVersion: %v", keyID)
+	}
+
+	vault := fmt.Sprintf("https://%s.vault.azure.net", parts[0])
+	key, version := parts[1], parts[2]
+
+	return &AZKeyID{Vault: vault, Key: key, Version: version}, nil
+}
+
 // NewAzureKeyVault creates a new KeyVault key manager instance.
 func NewAzureKeyVault(ctx context.Context) (KeyManager, error) {
 	authorizer, err := azurekeyvault.GetKeyVaultAuthorizer()
@@ -59,6 +77,57 @@ func NewAzureKeyVault(ctx context.Context) (KeyManager, error) {
 	return sm, nil
 }
 
+func (v *AzureKeyVault) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) {
+	k, err := ParseAZKeyID(keyID)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO(mikehelmick) - neds AEAD
+	value := base64.URLEncoding.EncodeToString(plaintext)
+	parameters := keyvault.KeyOperationsParameters{
+		Algorithm: keyvault.RSAOAEP256,
+		Value:     &value,
+	}
+	res, err := v.client.Encrypt(ctx, k.Vault, k.Key, k.Version, parameters)
+	if err != nil {
+		return nil, fmt.Errorf("unable to encrypt: %w", err)
+	}
+
+	resBytes, err := base64.URLEncoding.DecodeString(*res.Result)
+	if err != nil {
+		return nil, fmt.Errorf("unable to decode encrypted data: %w", err)
+	}
+
+	return resBytes, nil
+}
+
+func (v *AzureKeyVault) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) {
+	k, err := ParseAZKeyID(keyID)
+	if err != nil {
+		return nil, err
+	}
+
+	// TODO(mikehelmick) - neds AEAD
+	value := base64.URLEncoding.EncodeToString(ciphertext)
+	parameters := keyvault.KeyOperationsParameters{
+		Algorithm: keyvault.RSAOAEP256,
+		Value:     &value,
+	}
+	res, err := v.client.Decrypt(ctx, k.Vault, k.Key, k.Version, parameters)
+	if err != nil {
+		return nil, fmt.Errorf("unable to decrypt: %w", err)
+	}
+
+	plaintext, err := base64.URLEncoding.DecodeString(*res.Result)
+	if err != nil {
+		return nil, fmt.Errorf("unable to decode decrypted data: %w", err)
+	}
+
+	return plaintext, nil
+
+}
+
 // NewSigner creates a new signer that uses a key in HashiCorp Vault's transit
 // backend. The keyID in the format:
 //
@@ -70,14 +139,11 @@ func NewAzureKeyVault(ctx context.Context) (KeyManager, error) {
 //
 // Both name and version are required.
 func (v *AzureKeyVault) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
-	parts := strings.SplitN(keyID, "/", 3)
-	if len(parts) < 3 {
-		return nil, fmt.Errorf("key must include vaultName, keyName, and keyVersion: %v", keyID)
+	k, err := ParseAZKeyID(keyID)
+	if err != nil {
+		return nil, err
 	}
-
-	vault := fmt.Sprintf("https://%s.vault.azure.net", parts[0])
-	key, version := parts[1], parts[2]
-	return NewAzureKeyVaultSigner(ctx, v.client, vault, key, version)
+	return NewAzureKeyVaultSigner(ctx, v.client, k.Vault, k.Key, k.Version)
 }
 
 type AzureKeyVaultSigner struct {
diff --git a/pkg/keys/google_cloud_kms.go b/pkg/keys/google_cloud_kms.go
index 6298e77..3057554 100644
--- a/pkg/keys/google_cloud_kms.go
+++ b/pkg/keys/google_cloud_kms.go
@@ -17,9 +17,11 @@ package keys
 import (
 	"context"
 	"crypto"
+	"fmt"
 
 	kms "cloud.google.com/go/kms/apiv1"
 	"github.com/sethvargo/go-gcpkms/pkg/gcpkms"
+	kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
 )
 
 // Compile-time check to verify implements interface.
@@ -42,3 +44,29 @@ func NewGoogleCloudKMS(ctx context.Context) (KeyManager, error) {
 func (kms *GoogleCloudKMS) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
 	return gcpkms.NewSigner(ctx, kms.client, keyID)
 }
+
+func (kms *GoogleCloudKMS) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) {
+	req := kmspb.EncryptRequest{
+		Name:                        keyID,
+		Plaintext:                   plaintext,
+		AdditionalAuthenticatedData: aad,
+	}
+	result, err := kms.client.Encrypt(ctx, &req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to encrypt: %w", err)
+	}
+	return result.Ciphertext, nil
+}
+
+func (kms *GoogleCloudKMS) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) {
+	req := kmspb.DecryptRequest{
+		Name:                        keyID,
+		Ciphertext:                  ciphertext,
+		AdditionalAuthenticatedData: aad,
+	}
+	result, err := kms.client.Decrypt(ctx, &req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decrypt: %w", err)
+	}
+	return result.Plaintext, nil
+}
diff --git a/pkg/keys/hashicorp_vault.go b/pkg/keys/hashicorp_vault.go
index 934bc7b..907fb78 100644
--- a/pkg/keys/hashicorp_vault.go
+++ b/pkg/keys/hashicorp_vault.go
@@ -35,7 +35,10 @@ var _ KeyManager = (*HashiCorpVault)(nil)
 var _ crypto.Signer = (*HashiCorpVaultSigner)(nil)
 
 // HashiCorpVault implements the keys.KeyManager interface and can be used to
-// sign export files.
+// sign export files and encrypt/decrypt data.
+//
+// For encryption keys, when using valut, the keys must be created with
+//   `derived=true`
 type HashiCorpVault struct {
 	client *vaultapi.Client
 }
@@ -54,6 +57,21 @@ func NewHashiCorpVault(ctx context.Context) (KeyManager, error) {
 	return sm, nil
 }
 
+type HCValueKeyID struct {
+	Name    string
+	Version string
+}
+
+func NewHCValueKeyID(keyID string) (*HCValueKeyID, error) {
+	parts := strings.SplitN(keyID, "@", 2)
+	switch len(parts) {
+	case 0, 1:
+		return nil, fmt.Errorf("missing version in: %v", keyID)
+	default:
+		return &HCValueKeyID{Name: parts[0], Version: parts[1]}, nil
+	}
+}
+
 // NewSigner creates a new signer that uses a key in HashiCorp Vault's transit
 // backend. The keyID is in the format:
 //
@@ -61,13 +79,65 @@ func NewHashiCorpVault(ctx context.Context) (KeyManager, error) {
 //
 // Both name and version are required.
 func (v *HashiCorpVault) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
-	parts := strings.SplitN(keyID, "@", 2)
-	switch len(parts) {
-	case 0, 1:
-		return nil, fmt.Errorf("missing version in: %v", keyID)
-	default:
-		return NewHashiCorpVaultSigner(ctx, v.client, parts[0], parts[1])
+	hvcKey, err := NewHCValueKeyID(keyID)
+	if err != nil {
+		return nil, err
+	}
+	return NewHashiCorpVaultSigner(ctx, v.client, hvcKey.Name, hvcKey.Version)
+}
+
+func (v *HashiCorpVault) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) {
+	kid, err := NewHCValueKeyID(keyID)
+	if err != nil {
+		return nil, err
+	}
+	pth := fmt.Sprintf("transit/encrypt/%s", kid.Name)
+	params := map[string]interface{}{
+		"plaintext": base64.StdEncoding.EncodeToString(plaintext),
+		"context":   base64.StdEncoding.EncodeToString(aad),
+		"type":      "aes256-gcm96",
 	}
+
+	result, err := v.client.Logical().Write(pth, params)
+	if err != nil {
+		return nil, fmt.Errorf("unable to encrypt: %w", err)
+	}
+
+	ciphertext, ok := result.Data["ciphertext"]
+	if !ok {
+		return nil, fmt.Errorf("encryption returned no ciphertext")
+	}
+
+	return []byte(ciphertext.(string)), nil
+}
+
+func (v *HashiCorpVault) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) {
+	kid, err := NewHCValueKeyID(keyID)
+	if err != nil {
+		return nil, err
+	}
+	pth := fmt.Sprintf("transit/decrypt/%s", kid.Name)
+	params := map[string]interface{}{
+		"ciphertext": string(ciphertext),
+		"context":    base64.StdEncoding.EncodeToString(aad),
+	}
+
+	result, err := v.client.Logical().Write(pth, params)
+	if err != nil {
+		return nil, fmt.Errorf("unable to encrypt: %w", err)
+	}
+
+	plaintext, ok := result.Data["plaintext"]
+	if !ok {
+		return nil, fmt.Errorf("dencryption returned no plaintext")
+	}
+
+	decoded, err := base64util.DecodeString(plaintext.(string))
+	if err != nil {
+		return nil, fmt.Errorf("unable to decode plaintext: %w", err)
+	}
+
+	return decoded, nil
 }
 
 type HashiCorpVaultSigner struct {
diff --git a/pkg/keys/keys.go b/pkg/keys/keys.go
index dc37524..8487ebd 100644
--- a/pkg/keys/keys.go
+++ b/pkg/keys/keys.go
@@ -31,6 +31,21 @@ import (
 // KeyManager implementations must be able to return a crypto.Signer.
 type KeyManager interface {
 	NewSigner(ctx context.Context, keyID string) (crypto.Signer, error)
+
+	// Encrypt wile enctypt a byte array along with accompaning Additional Authenticated Data (AAD).
+	// The ability for AAD to be empty, depends on the implementation being used.
+	//
+	// Currently Google Cloud KMS, Hashicorp Vault and AWS KMS support AAD
+	// The Azure Key Vault implementation does not.
+	Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error)
+
+	// Decrypt will descrypt a previously encrypted byte array along with accompaning Additional
+	// Authenticated Data (AAD).
+	// If AAD was passed in on the encryption, the same AAD must be passed in to decrypt.
+	//
+	// Currently Google Cloud KMS, Hashicorp Vault and AWS KMS support AAD
+	// The Azure Key Vault implementation does not.
+	Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error)
 }
 
 // KeyManagerFor returns the appropriate key manager for the given type.
diff --git a/pkg/keys/noop.go b/pkg/keys/noop.go
index 55e5d64..7dfd02a 100644
--- a/pkg/keys/noop.go
+++ b/pkg/keys/noop.go
@@ -33,3 +33,11 @@ func NewNoop(ctx context.Context) (KeyManager, error) {
 func (n *Noop) NewSigner(ctx context.Context, keyID string) (crypto.Signer, error) {
 	return nil, fmt.Errorf("noop cannot sign")
 }
+
+func (n *Noop) Encrypt(ctx context.Context, keyID string, plaintext []byte, aad []byte) ([]byte, error) {
+	return plaintext, nil
+}
+
+func (n *Noop) Decrypt(ctx context.Context, keyID string, ciphertext []byte, aad []byte) ([]byte, error) {
+	return ciphertext, nil
+}


Test patch:
diff --git a/pkg/keys/hashicorp_vault_test.go b/pkg/keys/hashicorp_vault_test.go
index 9f0cdac..8f9d7df 100644
--- a/pkg/keys/hashicorp_vault_test.go
+++ b/pkg/keys/hashicorp_vault_test.go
@@ -71,6 +71,98 @@ func TestNewHashiCorpVaultSigner(t *testing.T) {
 	}
 }
 
+func TestHashiCorpVaultEncryptDecrypt(t *testing.T) {
+	setupFn := func(client *vaultapi.Client) error {
+		if _, err := client.Logical().Write("transit/keys/my-key", map[string]interface{}{
+			"type": "aes256-gcm96",
+		}); err != nil {
+			return fmt.Errorf("failed to create key: %w", err)
+		}
+		return nil
+	}
+	identityFn := func(b []byte) []byte {
+		return b
+	}
+
+	cases := []struct {
+		name  string
+		setup func(client *vaultapi.Client) error
+		keyID string
+
+		plaintext    string
+		aad          []byte
+		modifyCipher func(b []byte) []byte
+		modifyAAD    func(b []byte) []byte
+
+		err    bool
+		errMsg string
+	}{
+		{
+			name:         "correct",
+			setup:        setupFn,
+			keyID:        "my-key@1",
+			plaintext:    "this is where you put secret stuff",
+			aad:          []byte("pizza"),
+			modifyCipher: identityFn,
+			modifyAAD:    identityFn,
+			err:          false,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Create a Vault server.
+			ctx := context.Background()
+			core, _, token := vault.TestCoreUnsealedWithConfig(t, &vault.CoreConfig{
+				DisableMlock: true,
+				DisableCache: true,
+				Logger:       vaultlog.NewNullLogger(),
+				LogicalBackends: map[string]vaultlogical.Factory{
+					"transit": vaulttransit.Factory,
+				},
+			})
+			ln, addr := vaulthttp.TestServer(t, core)
+			defer ln.Close()
+
+			// Create the client.
+			client, err := vaultapi.NewClient(&vaultapi.Config{Address: addr})
+			if err != nil {
+				t.Fatal(err)
+			}
+			client.SetToken(token)
+
+			// Enable transit.
+			if _, err := client.Logical().Write("sys/mounts/transit", map[string]interface{}{
+				"type": "transit",
+			}); err != nil {
+				t.Fatal(err)
+			}
+
+			// Run setup.
+			if err := tc.setup(client); err != nil {
+				t.Fatal(err)
+			}
+
+			// Wrap client.
+			vault := &HashiCorpVault{client}
+
+			ciphertext, err := vault.Encrypt(ctx, tc.keyID, []byte(tc.plaintext), tc.aad)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			decrypted, err := vault.Decrypt(ctx, tc.keyID, ciphertext, tc.aad)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if tc.plaintext != string(decrypted) {
+				t.Fatalf("encrypt/decrypt got different result: want: %v got: %v", tc.plaintext, string(decrypted))
+			}
+		})
+	}
+}
+
 func TestHashiCorpVaultSigner_Public(t *testing.T) {
 	cases := []struct {
 		name       string