Repository: TBD54566975/ssi-service
Pull request #599 (2023-07-25 23:54:50)

Added app level encryption feature.
# Overview
Allow for operators of ssi-service to choose whether to do app level encryption.

# Description

At a high-level, this PR is adding a Data Encryption Key. The DEK can be stored either in the configured storage, or in a KMS. The DEK is used for every read/write that happens in the application.

Specifically, this PR does the following:
* Removed some methods that weren't necessary in the storage interface. 
* Implemented and `EncryptedWrapper`, which takes in any implementation of a storage, and adds an encryption layer. 
* Refactored the existing Key Encryption Mechanism so it could be reused with the data encryption layer. 
* Added documentation.

# How Has This Been Tested?
All unit tests and integration tests pass when encryption is enabled (see the dev.toml file).


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#599](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (7fdd5bf) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/31ea6fd62bf3aa5a63d464a7f980d64e268319db?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (31ea6fd) will **increase** coverage by `0.68%`.
> The diff coverage is `62.24%`.

> :exclamation: Current head 7fdd5bf differs from pull request most recent head 7c03551. Consider uploading reports for the commit 7c03551 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #599      +/-   ##
==========================================
+ Coverage   25.70%   26.39%   +0.68%     
==========================================
  Files          46       47       +1     
  Lines        5699     5767      +68     
==========================================
+ Hits         1465     1522      +57     
- Misses       3951     3962      +11     
  Partials      283      283              
```


| [Files Changed](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [config/config.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-Y29uZmlnL2NvbmZpZy5nbw==) | `25.89% <0.00%> (-1.47%)` | :arrow_down: |
| [pkg/service/did/web.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3dlYi5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/storage/bolt.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3N0b3JhZ2UvYm9sdC5nbw==) | `70.22% <ø> (+4.18%)` | :arrow_up: |
| [pkg/service/operation/storage.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uvb3BlcmF0aW9uL3N0b3JhZ2UuZ28=) | `25.25% <50.00%> (+0.51%)` | :arrow_up: |
| [pkg/service/keystore/storage.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uva2V5c3RvcmUvc3RvcmFnZS5nbw==) | `39.64% <54.54%> (+4.90%)` | :arrow_up: |
| [pkg/storage/encrypt.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3N0b3JhZ2UvZW5jcnlwdC5nbw==) | `57.77% <57.77%> (ø)` | |
| [pkg/service/keystore/service.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2Uva2V5c3RvcmUvc2VydmljZS5nbw==) | `42.75% <60.00%> (+0.64%)` | :arrow_up: |
| [pkg/storage/storage.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3N0b3JhZ2Uvc3RvcmFnZS5nbw==) | `70.00% <85.48%> (+25.26%)` | :arrow_up: |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/599/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)


andresuribe87: > it would be great to have tests that exercise different confidence levels (no encryption, local encryption, hsm encryption)
> 
> for hsm we would need a reliable cloud test env, so maybe that's out of scope for now but would be good to track in a future issue.

no encryption is covered in unit tests. Local encryption is covered in integration tests. HSM will be tracked in https://github.com/TBD54566975/ssi-service/issues/600
> 
> similarly, we should track issues for managing key rotation.

https://github.com/TBD54566975/ssi-service/issues/601

> 
> if you didn't (I may have missed) it would be useful to highlight that we've specifically chosen an authenticated encryption scheme which gives us the added benefit of tamper evidence, and may also prove useful for decrypting/re-encrypting data during key rotations since we'll be able to see which key encrypted the data

I'm going to leave that for the same issue as above (i.e. #601 )

> 
> another thing worth covering in the doc is the data that we need to keep in plaintext, and the considerations around that, for keys. these keys are often just UUIDs but sometimes contain other information to make prefix queries viable. that means they could contain a DID, which by someones definition may be PII. <-- not sure here but we should verify

We _could_ encrypt keys as long as we preserve the key prefix structure. That said, it doesn't seem like it's worth the effort and additional complexity. I added some more info to the docs, and created another issue to track #603




Patch:
diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go
index 796e130..560e070 100644
--- a/cmd/ssiservice/main.go
+++ b/cmd/ssiservice/main.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"os"
 	"os/signal"
+	"path"
 	"strconv"
 	"syscall"
 	"time"
@@ -52,7 +53,9 @@ func run() error {
 		logrus.Infof("loading config from env var path: %s", envConfigPath)
 		configPath = envConfigPath
 	}
-	cfg, err := config.LoadConfig(configPath)
+
+	dir, file := path.Split(configPath)
+	cfg, err := config.LoadConfig(file, os.DirFS(dir))
 	if err != nil {
 		logrus.Fatalf("could not instantiate config: %s", err.Error())
 	}
diff --git a/config/config.go b/config/config.go
index 7d1433a..370256b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"fmt"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -72,6 +73,10 @@ type ServicesConfig struct {
 	StorageOptions  []storage.Option `toml:"storage_option"`
 	ServiceEndpoint string           `toml:"service_endpoint"`
 
+	// Application level encryption configuration. Defines how values are encrypted before they are stored in the
+	// configured KV store.
+	AppLevelEncryptionConfiguration EncryptionConfig `toml:"storage_encryption,omitempty"`
+
 	// Embed all service-specific configs here. The order matters: from which should be instantiated first, to last
 	KeyStoreConfig        KeyStoreServiceConfig     `toml:"keystore,omitempty"`
 	DIDConfig             DIDServiceConfig          `toml:"did,omitempty"`
@@ -94,14 +99,34 @@ type BaseServiceConfig struct {
 type KeyStoreServiceConfig struct {
 	*BaseServiceConfig
 
-	// The URI for the master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink
-	// When left empty, then a random key is generated and used.
+	// Configuration describing the encryption of the private keys that are under ssi-service's custody.
+	EncryptionConfig
+}
+
+type EncryptionConfig struct {
+	DisableEncryption bool `toml:"disable_encryption"`
+
+	// The URI for a master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink
+	// When left empty and DisableEncryption is off, then a random key is generated and used. This random key is persisted unencrypted in the
+	// configured storage. Production deployments should never leave this field empty.
 	MasterKeyURI string `toml:"master_key_uri"`
 
-	// Path for credentials. Required when using an external KMS. More info at https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials
+	// Path for credentials. Required when MasterKeyURI is set. More info at https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials
 	KMSCredentialsPath string `toml:"kms_credentials_path"`
 }
 
+func (e EncryptionConfig) GetMasterKeyURI() string {
+	return e.MasterKeyURI
+}
+
+func (e EncryptionConfig) GetKMSCredentialsPath() string {
+	return e.KMSCredentialsPath
+}
+
+func (e EncryptionConfig) EncryptionEnabled() bool {
+	return !e.DisableEncryption
+}
+
 func (k *KeyStoreServiceConfig) IsEmpty() bool {
 	if k == nil {
 		return true
@@ -109,6 +134,18 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool {
 	return reflect.DeepEqual(k, &KeyStoreServiceConfig{})
 }
 
+func (k *KeyStoreServiceConfig) GetMasterKeyURI() string {
+	return k.MasterKeyURI
+}
+
+func (k *KeyStoreServiceConfig) GetKMSCredentialsPath() string {
+	return k.KMSCredentialsPath
+}
+
+func (k *KeyStoreServiceConfig) EncryptionEnabled() bool {
+	return !k.DisableEncryption
+}
+
 type DIDServiceConfig struct {
 	*BaseServiceConfig
 	Methods                  []string `toml:"methods"`
@@ -211,7 +248,10 @@ func (p *WebhookServiceConfig) IsEmpty() bool {
 
 // LoadConfig attempts to load a TOML config file from the given path, and coerce it into our object model.
 // Before loading, defaults are applied on certain properties, which are overwritten if specified in the TOML file.
-func LoadConfig(path string) (*SSIServiceConfig, error) {
+func LoadConfig(path string, fs fs.FS) (*SSIServiceConfig, error) {
+	if fs == nil {
+		fs = os.DirFS(".")
+	}
 	loadDefaultConfig, err := checkValidConfigPath(path)
 	if err != nil {
 		return nil, errors.Wrap(err, "validate config path")
@@ -226,7 +266,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
 	if loadDefaultConfig {
 		defaultServicesConfig := getDefaultServicesConfig()
 		config.Services = defaultServicesConfig
-	} else if err = loadTOMLConfig(path, &config); err != nil {
+	} else if err = loadTOMLConfig(path, &config, fs); err != nil {
 		return nil, errors.Wrap(err, "load toml config")
 	}
 
@@ -234,9 +274,25 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
 		return nil, errors.Wrap(err, "apply env variables")
 	}
 
+	if err = validateConfig(&config); err != nil {
+		return nil, errors.Wrap(err, "validating config values")
+	}
+
 	return &config, nil
 }
 
+func validateConfig(s *SSIServiceConfig) error {
+	if s.Server.Environment == EnvironmentProd {
+		if s.Services.KeyStoreConfig.DisableEncryption {
+			return errors.New("prod environment cannot disable key encryption")
+		}
+		if s.Services.AppLevelEncryptionConfiguration.DisableEncryption {
+			logrus.Warn("prod environment detected without app level encryption. This is strongly discouraged.")
+		}
+	}
+	return nil
+}
+
 func checkValidConfigPath(path string) (bool, error) {
 	// no path, load default config
 	defaultConfig := false
@@ -314,9 +370,13 @@ func getDefaultServicesConfig() ServicesConfig {
 	}
 }
 
-func loadTOMLConfig(path string, config *SSIServiceConfig) error {
+func loadTOMLConfig(path string, config *SSIServiceConfig, fs fs.FS) error {
 	// load from TOML file
-	if _, err := toml.DecodeFile(path, &config); err != nil {
+	file, err := fs.Open(path)
+	if err != nil {
+		return errors.Wrapf(err, "opening path %s", path)
+	}
+	if _, err := toml.NewDecoder(file).Decode(&config); err != nil {
 		return errors.Wrapf(err, "could not load config: %s", path)
 	}
 
diff --git a/config/dev.toml b/config/dev.toml
index a3f4168..70e87d5 100644
--- a/config/dev.toml
+++ b/config/dev.toml
@@ -29,6 +29,10 @@ service_endpoint = "http://localhost:3000"
 # example bolt config with filepath option
 storage = "bolt"
 
+[services.storage_encryption]
+# encryption
+disable_encryption = true
+
 [[services.storage_option]]
 id = "boltdb-filepath-option"
 option = "bolt.db"
diff --git a/config/prod.toml b/config/prod.toml
index 025a003..dc11c44 100644
--- a/config/prod.toml
+++ b/config/prod.toml
@@ -20,7 +20,12 @@ log_location = "log"
 log_level = "info"
 
 enable_schema_caching = true
-enable_allow_all_cors = true
+enable_allow_all_cors = false
+
+[services.storage_encryption]
+# master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*"
+# kms_credentials_path = "credentials.json"
+disable_encryption = false
 
 # Storage Configuration
 [services]
@@ -38,6 +43,7 @@ option = "password"
 # per-service configuration
 [services.keystore]
 name = "keystore"
+disable_encryption = false
 # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*"
 # kms_credentials_path = "credentials.json"
 
diff --git a/doc/STORAGE.md b/doc/STORAGE.md
index 7d539f5..34420f9 100644
--- a/doc/STORAGE.md
+++ b/doc/STORAGE.md
@@ -70,4 +70,55 @@ For a working example, see this [dev.toml file](https://github.com/TBD54566975/s
 
 You need to implement the [ServiceStorage interface](../pkg/storage/storage.go), similar to how [Redis](../pkg/storage/redis.go)
 is implemented. For an example, see [this PR](https://github.com/TBD54566975/ssi-service/pull/590/files#diff-606358579107e7ad1221525001aed8c776a141d4cc5aab9ef7a3ddbcec10d9f9)
-which introduces the SQL based implementation.
\ No newline at end of file
+which introduces the SQL based implementation.
+
+## Encryption
+
+SSI Service supports application level encryption of values before sending them to the configured KV store. Please note
+that keys (i.e. the key of the KV store) are not currently encrypted. See the [Privacy Considerations](#privacy-considerations) for more information.
+A MasterKey is used (a.k.a. a Data Encryption Key or DEK) to encrypt all data before it's sent to the configured storage.
+The MasterKey can be stored in the configured storage system or in an external Key Management System (KMS) like GCP KMS or AWS KMS.
+When storing locally, the key will be automatically generated if it doesn't exist already.
+
+**For production deployments, it is strongly recommended to store the MasterKey in an external KMS.**
+
+To use an external KMS:
+1. Create a symmetric encryption key in your KMS. You MUST select the algorithm that uses AES-256 block cipher in Galois/Counter Mode (GCM). At the time of writing, this is the only algorithm supported by AWS and GCP.
+2. Set the `master_key_uri` field of the `[services.storage_encryption]` section using the format described in [tink](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-systems)
+   (we use the tink library under the hood).
+3. Set the `kms_credentials_path` field of the `[services.storage_encryption]` section to point to your credentials file, according to [this section](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials).
+4. Win!
+
+Below, there is an example snippet of what the TOML configuration should look like.
+```toml
+[services.storage_encryption]
+# Make sure the following values are valid.
+master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*"
+kms_credentials_path = "credentials.json"
+disable_encryption = false
+```
+
+Storing the MasterKey in the configured storage system is done with the following options in your TOML configuration.
+
+```toml
+[services.storage_encryption]
+# ensure that master_key_uri is NOT set.
+disable_encryption = false
+```
+
+Disabling app level encryption is also possible using the following options in your TOML configuration:
+
+```toml
+[services.storage_encryption]
+# encryption
+disable_encryption = true
+```
+
+### Privacy Considerations
+
+From the perspective of SSI-Service, all keys are stored in plaintext (this doesn't preclude configuring encryption at rest
+in your deployment of the storage configuration). Making all keys readable by any actor may have an impact in your organization's
+use cases around privacy. You should consider whether this is acceptable. Notably, a DID that was created by SSI Service
+is stored as a key. This can fit some definition of PII, as it could be correlated to identify and individual.
+
+Encrypting keys is being considered in https://github.com/TBD54566975/ssi-service/issues/603.
\ No newline at end of file
diff --git a/pkg/encryption/encryption.go b/pkg/encryption/encryption.go
new file mode 100644
index 0000000..64a0feb
--- /dev/null
+++ b/pkg/encryption/encryption.go
@@ -0,0 +1,164 @@
+package encryption
+
+import (
+	"context"
+	"strings"
+
+	sdkutil "github.com/TBD54566975/ssi-sdk/util"
+	"github.com/google/tink/go/aead"
+	"github.com/google/tink/go/core/registry"
+	"github.com/google/tink/go/integration/awskms"
+	"github.com/google/tink/go/integration/gcpkms"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/tink"
+	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/internal/util"
+	"google.golang.org/api/option"
+)
+
+// Encrypter the interface for any encrypter implementation.
+type Encrypter interface {
+	Encrypt(ctx context.Context, plaintext, contextData []byte) ([]byte, error)
+}
+
+// Decrypter is the interface for any decrypter. May be AEAD or Hybrid.
+type Decrypter interface {
+	// Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in
+	// https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html)
+	Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error)
+}
+
+type KeyResolver func(ctx context.Context) ([]byte, error)
+
+type XChaCha20Poly1305Encrypter struct {
+	keyResolver KeyResolver
+}
+
+func NewXChaCha20Poly1305EncrypterWithKey(key []byte) *XChaCha20Poly1305Encrypter {
+	return &XChaCha20Poly1305Encrypter{func(ctx context.Context) ([]byte, error) {
+		return key, nil
+	}}
+}
+
+func NewXChaCha20Poly1305EncrypterWithKeyResolver(resolver KeyResolver) *XChaCha20Poly1305Encrypter {
+	return &XChaCha20Poly1305Encrypter{resolver}
+}
+
+func (k XChaCha20Poly1305Encrypter) Encrypt(ctx context.Context, plaintext, _ []byte) ([]byte, error) {
+	// encrypt key before storing
+	key, err := k.keyResolver(ctx)
+	if err != nil {
+		return nil, errors.Wrap(err, "resolving key")
+	}
+	encryptedKey, err := util.XChaCha20Poly1305Encrypt(key, plaintext)
+	if err != nil {
+		return nil, sdkutil.LoggingErrorMsgf(err, "could not encrypt key")
+	}
+	return encryptedKey, nil
+}
+
+func (k XChaCha20Poly1305Encrypter) Decrypt(ctx context.Context, ciphertext, _ []byte) ([]byte, error) {
+	if ciphertext == nil {
+		return nil, nil
+	}
+
+	key, err := k.keyResolver(ctx)
+	if err != nil {
+		return nil, errors.Wrap(err, "resolving key")
+	}
+	// decrypt key before unmarshaling
+	decryptedKey, err := util.XChaCha20Poly1305Decrypt(key, ciphertext)
+	if err != nil {
+		return nil, sdkutil.LoggingErrorMsgf(err, "could not decrypt key")
+	}
+
+	return decryptedKey, nil
+}
+
+var _ Decrypter = (*XChaCha20Poly1305Encrypter)(nil)
+var _ Encrypter = (*XChaCha20Poly1305Encrypter)(nil)
+
+type noopDecrypter struct{}
+
+func (n noopDecrypter) Decrypt(_ context.Context, ciphertext, _ []byte) ([]byte, error) {
+	return ciphertext, nil
+}
+
+type noopEncrypter struct{}
+
+func (n noopEncrypter) Encrypt(_ context.Context, plaintext, _ []byte) ([]byte, error) {
+	return plaintext, nil
+}
+
+var _ Decrypter = (*noopDecrypter)(nil)
+var _ Encrypter = (*noopEncrypter)(nil)
+
+var (
+	NoopDecrypter = noopDecrypter{}
+	NoopEncrypter = noopEncrypter{}
+)
+
+type wrappedEncrypter struct {
+	tink.AEAD
+}
+
+func (w wrappedEncrypter) Encrypt(_ context.Context, plaintext, contextData []byte) ([]byte, error) {
+	return w.AEAD.Encrypt(plaintext, contextData)
+}
+
+var _ Encrypter = (*wrappedEncrypter)(nil)
+
+type wrappedDecrypter struct {
+	tink.AEAD
+}
+
+func (w wrappedDecrypter) Decrypt(_ context.Context, ciphertext, contextInfo []byte) ([]byte, error) {
+	return w.AEAD.Decrypt(ciphertext, contextInfo)
+}
+
+var _ Decrypter = (*wrappedDecrypter)(nil)
+
+const (
+	gcpKMSScheme = "gcp-kms"
+	awsKMSScheme = "aws-kms"
+)
+
+type ExternalEncryptionConfig interface {
+	GetMasterKeyURI() string
+	GetKMSCredentialsPath() string
+	EncryptionEnabled() bool
+}
+
+func NewExternalEncrypter(ctx context.Context, cfg ExternalEncryptionConfig) (Encrypter, Decrypter, error) {
+	if !cfg.EncryptionEnabled() {
+		return NoopEncrypter, NoopDecrypter, nil
+	}
+	var client registry.KMSClient
+	var err error
+	switch {
+	case strings.HasPrefix(cfg.GetMasterKeyURI(), gcpKMSScheme):
+		client, err = gcpkms.NewClientWithOptions(ctx, cfg.GetMasterKeyURI(), option.WithCredentialsFile(cfg.GetKMSCredentialsPath()))
+		if err != nil {
+			return nil, nil, errors.Wrap(err, "creating gcp kms client")
+		}
+	case strings.HasPrefix(cfg.GetMasterKeyURI(), awsKMSScheme):
+		client, err = awskms.NewClientWithCredentials(cfg.GetMasterKeyURI(), cfg.GetKMSCredentialsPath())
+		if err != nil {
+			return nil, nil, errors.Wrap(err, "creating aws kms client")
+		}
+	default:
+		return nil, nil, errors.Errorf("master_key_uri value %q is not supported", cfg.GetMasterKeyURI())
+	}
+	// TODO: move client registration to be per request (i.e. when things are encrypted/decrypted). https://github.com/TBD54566975/ssi-service/issues/598
+	registry.RegisterKMSClient(client)
+	dek := aead.AES256GCMKeyTemplate()
+	kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(cfg.GetMasterKeyURI(), dek))
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "creating keyset handle")
+	}
+	a, err := aead.New(kh)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "creating aead from key handl")
+	}
+	return wrappedEncrypter{a}, wrappedDecrypter{a}, nil
+}
diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go
index 11601c9..158b207 100644
--- a/pkg/service/did/web.go
+++ b/pkg/service/did/web.go
@@ -65,7 +65,7 @@ func (h *webHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*
 
 	err := didWeb.Validate(ctx)
 	if err != nil {
-		return nil, errors.Wrap(err, "could not validate did:web")
+		return nil, errors.Wrap(err, "could not validate if did:web exists externally")
 	}
 
 	exists, err := h.storage.DIDExists(ctx, opts.DIDWebID)
diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go
index a773de0..cb8c4b5 100644
--- a/pkg/service/keystore/service.go
+++ b/pkg/service/keystore/service.go
@@ -10,14 +10,13 @@ import (
 	"github.com/mr-tron/base58"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
-	"golang.org/x/crypto/chacha20poly1305"
-
-	"github.com/tbd54566975/ssi-service/internal/keyaccess"
-
 	"github.com/tbd54566975/ssi-service/config"
+	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/encryption"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
+	"golang.org/x/crypto/chacha20poly1305"
 )
 
 type ServiceFactory func(storage.Tx) (*Service, error)
@@ -50,20 +49,17 @@ func (s Service) Config() config.KeyStoreServiceConfig {
 }
 
 func NewKeyStoreService(config config.KeyStoreServiceConfig, s storage.ServiceStorage) (*Service, error) {
-	if err := EnsureServiceKeyExists(config, s); err != nil {
-		return nil, sdkutil.LoggingErrorMsg(err, "initializing keystore")
+	encrypter, decrypter, err := NewServiceEncryption(s, config.EncryptionConfig, ServiceKeyEncryptionKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "creating new encryption")
 	}
-	factory := NewKeyStoreServiceFactory(config, s)
+
+	factory := NewKeyStoreServiceFactory(config, s, encrypter, decrypter)
 	return factory(s)
 }
 
-func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage) ServiceFactory {
+func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage, encrypter encryption.Encrypter, decrypter encryption.Decrypter) ServiceFactory {
 	return func(tx storage.Tx) (*Service, error) {
-		encrypter, decrypter, err := newEncryption(s, config)
-		if err != nil {
-			return nil, errors.Wrap(err, "creating new encryption")
-		}
-
 		// Next, instantiate the key storage
 		keyStoreStorage, err := NewKeyStoreStorage(s, encrypter, decrypter, tx)
 		if err != nil {
@@ -180,24 +176,6 @@ func GenerateServiceKey() (key string, err error) {
 	return
 }
 
-// EncryptKey encrypts another key with the service key using xchacha20-poly1305
-func EncryptKey(serviceKey, key []byte) ([]byte, error) {
-	encryptedKey, err := util.XChaCha20Poly1305Encrypt(serviceKey, key)
-	if err != nil {
-		return nil, errors.Wrap(err, "encrypting key with service key")
-	}
-	return encryptedKey, nil
-}
-
-// DecryptKey encrypts another key with the service key using xchacha20-poly1305
-func DecryptKey(serviceKey, encryptedKey []byte) ([]byte, error) {
-	decryptedKey, err := util.XChaCha20Poly1305Decrypt(serviceKey, encryptedKey)
-	if err != nil {
-		return nil, errors.Wrap(err, "decrypting key with service key")
-	}
-	return decryptedKey, nil
-}
-
 // Sign fetches the key in the store, and uses it to sign data. Data should be json or json-serializable.
 func (s Service) Sign(ctx context.Context, keyID string, data any) (*keyaccess.JWT, error) {
 	gotKey, err := s.GetKey(ctx, GetKeyRequest{ID: keyID})
diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go
index ad25ba1..097c893 100644
--- a/pkg/service/keystore/storage.go
+++ b/pkg/service/keystore/storage.go
@@ -2,7 +2,6 @@ package keystore
 
 import (
 	"context"
-	"strings"
 	"time"
 
 	"github.com/TBD54566975/ssi-sdk/crypto"
@@ -10,19 +9,9 @@ import (
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/benbjohnson/clock"
 	"github.com/goccy/go-json"
-	"github.com/google/tink/go/aead"
-	"github.com/google/tink/go/core/registry"
-	"github.com/google/tink/go/integration/awskms"
-	"github.com/google/tink/go/integration/gcpkms"
-	"github.com/google/tink/go/keyset"
-	"github.com/google/tink/go/tink"
 	"github.com/mr-tron/base58"
 	"github.com/pkg/errors"
-	"google.golang.org/api/option"
-
-	"github.com/tbd54566975/ssi-service/config"
-
-	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/encryption"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 )
 
@@ -57,24 +46,26 @@ const (
 	namespace             = "keystore"
 	serviceInternalSuffix = "service-internal"
 	publicNamespaceSuffix = "public-keys"
-	skKey                 = "ssi-service-key"
 	keyNotFoundErrMsg     = "key not found"
+
+	ServiceKeyEncryptionKey  = "ssi-service-key-encryption-key"
+	ServiceDataEncryptionKey = "ssi-service-data-key"
 )
 
 var (
-	serviceNamespace   = storage.Join(namespace, serviceInternalSuffix)
-	publicKeyNamespace = storage.Join(namespace, publicNamespaceSuffix)
+	serviceInternalNamespace = storage.Join(namespace, serviceInternalSuffix)
+	publicKeyNamespace       = storage.Join(namespace, publicNamespaceSuffix)
 )
 
 type Storage struct {
 	db        storage.ServiceStorage
 	tx        storage.Tx
-	encrypter Encrypter
-	decrypter Decrypter
+	encrypter encryption.Encrypter
+	decrypter encryption.Decrypter
 	Clock     clock.Clock
 }
 
-func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, writer storage.Tx) (*Storage, error) {
+func NewKeyStoreStorage(db storage.ServiceStorage, e encryption.Encrypter, d encryption.Decrypter, writer storage.Tx) (*Storage, error) {
 	s := &Storage{
 		db:        db,
 		encrypter: e,
@@ -85,41 +76,26 @@ func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, wri
 	if writer != nil {
 		s.tx = writer
 	}
+	if s.encrypter == nil {
+		s.encrypter = encryption.NoopEncrypter
+	}
+	if s.decrypter == nil {
+		s.decrypter = encryption.NoopDecrypter
+	}
 
 	return s, nil
 }
 
-type wrappedEncrypter struct {
-	tink.AEAD
-}
-
-func (w wrappedEncrypter) Encrypt(_ context.Context, plaintext, contextData []byte) ([]byte, error) {
-	return w.AEAD.Encrypt(plaintext, contextData)
-}
-
-type wrappedDecrypter struct {
-	tink.AEAD
-}
-
-func (w wrappedDecrypter) Decrypt(_ context.Context, ciphertext, contextInfo []byte) ([]byte, error) {
-	return w.AEAD.Decrypt(ciphertext, contextInfo)
-}
-
-const (
-	gcpKMSScheme = "gcp-kms"
-	awsKMSScheme = "aws-kms"
-)
-
-// EnsureServiceKeyExists makes sure that the service key that will be used for encryption exists. This function is
+// ensureEncryptionKeyExists makes sure that the service key that will be used for encryption exists. This function is
 // idempotent, so that multiple instances of ssi-service can call it on boot.
-func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storage.ServiceStorage) error {
-	if config.MasterKeyURI != "" {
+func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provider storage.ServiceStorage, namespace, encryptionMaterialKey string) error {
+	if config.GetMasterKeyURI() != "" {
 		return nil
 	}
 
 	watchKeys := []storage.WatchKey{{
 		Namespace: namespace,
-		Key:       skKey,
+		Key:       encryptionMaterialKey,
 	}}
 
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -127,7 +103,7 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag
 
 	_, err := provider.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) {
 		// Create the key only if it doesn't already exist.
-		gotKey, err := getServiceKey(ctx, provider)
+		gotKey, err := getServiceKey(ctx, provider, namespace, encryptionMaterialKey)
 		if gotKey == nil && err.Error() == keyNotFoundErrMsg {
 			serviceKey, err := GenerateServiceKey()
 			if err != nil {
@@ -137,7 +113,7 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag
 			key := ServiceKey{
 				Base58Key: serviceKey,
 			}
-			if err := storeServiceKey(ctx, tx, key); err != nil {
+			if err := storeServiceKey(ctx, tx, key, namespace, encryptionMaterialKey); err != nil {
 				return nil, err
 			}
 			return nil, nil
@@ -150,62 +126,41 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag
 	return nil
 }
 
-// newEncryption creates a pair of Encrypter and Decrypter. The service key must have been created before this function
-// is called. EnsureServiceKeyExists can be used to make sure the service key exists.
-func newEncryption(db storage.ServiceStorage, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) {
-	if len(cfg.MasterKeyURI) != 0 {
+// NewServiceEncryption creates a pair of Encrypter and Decrypter with the given configuration.
+func NewServiceEncryption(db storage.ServiceStorage, cfg encryption.ExternalEncryptionConfig, key string) (encryption.Encrypter, encryption.Decrypter, error) {
+	if !cfg.EncryptionEnabled() {
+		return nil, nil, nil
+	}
+
+	if len(cfg.GetMasterKeyURI()) != 0 {
 		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		defer cancel()
-		return NewExternalEncrypter(ctx, cfg)
+		return encryption.NewExternalEncrypter(ctx, cfg)
 	}
 
-	return &encrypter{db}, &decrypter{db}, nil
-}
-
-func NewExternalEncrypter(ctx context.Context, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) {
-	var client registry.KMSClient
-	var err error
-	switch {
-	case strings.HasPrefix(cfg.MasterKeyURI, gcpKMSScheme):
-		client, err = gcpkms.NewClientWithOptions(ctx, cfg.MasterKeyURI, option.WithCredentialsFile(cfg.KMSCredentialsPath))
-		if err != nil {
-			return nil, nil, errors.Wrap(err, "creating gcp kms client")
-		}
-	case strings.HasPrefix(cfg.MasterKeyURI, awsKMSScheme):
-		client, err = awskms.NewClientWithCredentials(cfg.MasterKeyURI, cfg.KMSCredentialsPath)
-		if err != nil {
-			return nil, nil, errors.Wrap(err, "creating aws kms client")
-		}
-	default:
-		return nil, nil, errors.Errorf("master_key_uri value %q is not supported", cfg.MasterKeyURI)
-	}
-	registry.RegisterKMSClient(client)
-	dek := aead.AES256GCMKeyTemplate()
-	kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(cfg.MasterKeyURI, dek))
-	if err != nil {
-		return nil, nil, errors.Wrap(err, "creating keyset handle")
-	}
-	a, err := aead.New(kh)
-	if err != nil {
-		return nil, nil, errors.Wrap(err, "creating aead from key handl")
+	if err := ensureEncryptionKeyExists(cfg, db, serviceInternalNamespace, key); err != nil {
+		return nil, nil, errors.Wrap(err, "ensuring that the encryption key exists")
 	}
-	return wrappedEncrypter{a}, wrappedDecrypter{a}, nil
+	encSuite := encryption.NewXChaCha20Poly1305EncrypterWithKeyResolver(func(ctx context.Context) ([]byte, error) {
+		return getServiceKey(ctx, db, serviceInternalNamespace, key)
+	})
+	return encSuite, encSuite, nil
 }
 
 // TODO(gabe): support more robust service key operations, including rotation, and caching
-func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey) error {
+func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey, namespace string, skKey string) error {
 	keyBytes, err := json.Marshal(key)
 	if err != nil {
 		return sdkutil.LoggingErrorMsg(err, "could not marshal service key")
 	}
-	if err = tx.Write(ctx, serviceNamespace, skKey, keyBytes); err != nil {
+	if err = tx.Write(ctx, namespace, skKey, keyBytes); err != nil {
 		return sdkutil.LoggingErrorMsg(err, "could store marshal service key")
 	}
 	return nil
 }
 
-func getServiceKey(ctx context.Context, db storage.ServiceStorage) ([]byte, error) {
-	storedKeyBytes, err := db.Read(ctx, serviceNamespace, skKey)
+func getServiceKey(ctx context.Context, db storage.ServiceStorage, namespace, skKey string) ([]byte, error) {
+	storedKeyBytes, err := db.Read(ctx, namespace, skKey)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not get service key")
 	}
@@ -226,56 +181,6 @@ func getServiceKey(ctx context.Context, db storage.ServiceStorage) ([]byte, erro
 	return keyBytes, nil
 }
 
-// Encrypter the interface for any encrypter implementation.
-type Encrypter interface {
-	Encrypt(ctx context.Context, plaintext, contextData []byte) ([]byte, error)
-}
-
-// Decrypter is the interface for any decrypter. May be AEAD or Hybrid.
-type Decrypter interface {
-	// Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in
-	// https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html)
-	Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error)
-}
-
-type encrypter struct {
-	db storage.ServiceStorage
-}
-
-func (e encrypter) Encrypt(ctx context.Context, plaintext, _ []byte) ([]byte, error) {
-	// get service key
-	serviceKey, err := getServiceKey(ctx, e.db)
-	if err != nil {
-		return nil, err
-	}
-	// encrypt key before storing
-	encryptedKey, err := util.XChaCha20Poly1305Encrypt(serviceKey, plaintext)
-	if err != nil {
-		return nil, sdkutil.LoggingErrorMsgf(err, "could not encrypt key")
-	}
-	return encryptedKey, nil
-}
-
-type decrypter struct {
-	db storage.ServiceStorage
-}
-
-func (d decrypter) Decrypt(ctx context.Context, ciphertext, _ []byte) ([]byte, error) {
-	// get service key
-	serviceKey, err := getServiceKey(ctx, d.db)
-	if err != nil {
-		return nil, err
-	}
-
-	// decrypt key before unmarshaling
-	decryptedKey, err := util.XChaCha20Poly1305Decrypt(serviceKey, ciphertext)
-	if err != nil {
-		return nil, sdkutil.LoggingErrorMsgf(err, "could not decrypt key")
-	}
-
-	return decryptedKey, nil
-}
-
 func (kss *Storage) StoreKey(ctx context.Context, key StoredKey) error {
 	// TODO(gabe): conflict checking on key id
 	id := key.ID
diff --git a/pkg/service/manifest/storage/storage.go b/pkg/service/manifest/storage/storage.go
index 534f9e3..e4a4050 100644
--- a/pkg/service/manifest/storage/storage.go
+++ b/pkg/service/manifest/storage/storage.go
@@ -246,7 +246,7 @@ func (ms *Storage) StoreReviewApplication(ctx context.Context, applicationID str
 	if approved {
 		m["status"] = opsubmission.StatusApproved
 	}
-	if _, err := ms.db.Update(ctx, credential.ApplicationNamespace, applicationID, m); err != nil {
+	if _, err := storage.Update(ctx, ms.db, credential.ApplicationNamespace, applicationID, m); err != nil {
 		return nil, nil, errors.Wrap(err, "updating application")
 	}
 
@@ -254,7 +254,7 @@ func (ms *Storage) StoreReviewApplication(ctx context.Context, applicationID str
 		return nil, nil, errors.Wrap(err, "storing credential response")
 	}
 
-	responseData, operationData, err := ms.db.UpdateValueAndOperation(ctx, responseNamespace, response.ID,
+	responseData, operationData, err := storage.UpdateValueAndOperation(ctx, ms.db, responseNamespace, response.ID,
 		storage.NewUpdater(m), namespace.FromID(opID), opID,
 		opsubmission.OperationUpdater{UpdaterWithMap: storage.NewUpdater(map[string]any{"done": true})})
 	if err != nil {
diff --git a/pkg/service/operation/storage.go b/pkg/service/operation/storage.go
index a49fd53..c9319ea 100644
--- a/pkg/service/operation/storage.go
+++ b/pkg/service/operation/storage.go
@@ -30,8 +30,9 @@ func (s Storage) CancelOperation(ctx context.Context, id string) (*opstorage.Sto
 	var err error
 	switch {
 	case strings.HasPrefix(id, submission.ParentResource):
-		_, opData, err = s.db.UpdateValueAndOperation(
+		_, opData, err = storage.UpdateValueAndOperation(
 			ctx,
+			s.db,
 			submission.Namespace, opstorage.StatusObjectID(id), storage.NewUpdater(map[string]any{
 				"status": submission.StatusCancelled,
 				"reason": cancelledReason,
@@ -42,8 +43,9 @@ func (s Storage) CancelOperation(ctx context.Context, id string) (*opstorage.Sto
 				}),
 			})
 	case strings.HasPrefix(id, credential.ParentResource):
-		_, opData, err = s.db.UpdateValueAndOperation(
+		_, opData, err = storage.UpdateValueAndOperation(
 			ctx,
+			s.db,
 			credential.ApplicationNamespace, opstorage.StatusObjectID(id), storage.NewUpdater(map[string]any{
 				"status": credential.StatusCancelled,
 				"reason": cancelledReason,
diff --git a/pkg/service/presentation/storage.go b/pkg/service/presentation/storage.go
index 009d337..431154d 100644
--- a/pkg/service/presentation/storage.go
+++ b/pkg/service/presentation/storage.go
@@ -35,8 +35,9 @@ func (ps *Storage) UpdateSubmission(ctx context.Context, id string, approved boo
 	if approved {
 		m["status"] = opsubmission.StatusApproved
 	}
-	submissionData, operationData, err := ps.db.UpdateValueAndOperation(
+	submissionData, operationData, err := storage.UpdateValueAndOperation(
 		ctx,
+		ps.db,
 		opsubmission.Namespace,
 		id,
 		storage.NewUpdater(m),
diff --git a/pkg/service/service.go b/pkg/service/service.go
index a93e61e..d3962bc 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
+	"github.com/pkg/errors"
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/service/credential"
 	"github.com/tbd54566975/ssi-service/pkg/service/did"
@@ -81,20 +82,30 @@ func validateServiceConfig(config config.ServicesConfig) error {
 
 // instantiateServices begins all instantiates and their dependencies
 func instantiateServices(config config.ServicesConfig) (*SSIService, error) {
-	storageProvider, err := storage.NewStorage(storage.Type(config.StorageProvider), config.StorageOptions...)
+	unencryptedStorageProvider, err := storage.NewStorage(storage.Type(config.StorageProvider), config.StorageOptions...)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsgf(err, "could not instantiate storage provider: %s", config.StorageProvider)
 	}
 
+	storageEncrypter, storageDecrypter, err := keystore.NewServiceEncryption(unencryptedStorageProvider, config.AppLevelEncryptionConfiguration, keystore.ServiceDataEncryptionKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "creating app level encrypter")
+	}
+	storageProvider := unencryptedStorageProvider
+	if storageEncrypter != nil && storageDecrypter != nil {
+		storageProvider = storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter)
+	}
+
 	webhookService, err := webhook.NewWebhookService(config.WebhookConfig, storageProvider)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the webhook service")
 	}
 
-	if err := keystore.EnsureServiceKeyExists(config.KeyStoreConfig, storageProvider); err != nil {
-		return nil, sdkutil.LoggingErrorMsg(err, "could not ensure the service key exists")
+	keyEncrypter, keyDecrypter, err := keystore.NewServiceEncryption(unencryptedStorageProvider, config.KeyStoreConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey)
+	if err != nil {
+		return nil, errors.Wrap(err, "creating keystore encrypter")
 	}
-	keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider)
+	keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider, keyEncrypter, keyDecrypter)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the keystore service factory")
 	}
diff --git a/pkg/storage/bolt.go b/pkg/storage/bolt.go
index b7705a4..2b96443 100644
--- a/pkg/storage/bolt.go
+++ b/pkg/storage/bolt.go
@@ -355,55 +355,3 @@ type ResponseSettingUpdater interface {
 	// SetUpdatedResponse sets the response that the Update method will later use to modify the data.
 	SetUpdatedResponse([]byte)
 }
-
-// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map.
-// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true.
-func (b *BoltDB) UpdateValueAndOperation(_ context.Context, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) {
-	err = b.db.Update(func(tx *bolt.Tx) error {
-		if err = updateTxFn(namespace, key, updater, &first)(tx); err != nil {
-			return err
-		}
-		opUpdater.SetUpdatedResponse(first)
-		return updateTxFn(opNamespace, opKey, opUpdater, &op)(tx)
-	})
-	return first, op, err
-}
-
-func (b *BoltDB) Update(_ context.Context, namespace string, key string, values map[string]any) ([]byte, error) {
-	var updatedData []byte
-	err := b.db.Update(updateTxFn(namespace, key, NewUpdater(values), &updatedData))
-	return updatedData, err
-}
-
-func updateTxFn(namespace string, key string, updater Updater, updatedData *[]byte) func(tx *bolt.Tx) error {
-	return func(tx *bolt.Tx) error {
-		data, err := updateTx(tx, namespace, key, updater)
-		if err != nil {
-			return err
-		}
-		*updatedData = data
-		return nil
-	}
-}
-
-func updateTx(tx *bolt.Tx, namespace string, key string, updater Updater) ([]byte, error) {
-	bucket := tx.Bucket([]byte(namespace))
-	if bucket == nil {
-		return nil, sdkutil.LoggingNewErrorf("namespace<%s> does not exist", namespace)
-	}
-	v := bucket.Get([]byte(key))
-	if v == nil {
-		return nil, sdkutil.LoggingNewErrorf("key not found %s", key)
-	}
-	if err := updater.Validate(v); err != nil {
-		return nil, sdkutil.LoggingErrorMsg(err, "validating update")
-	}
-	data, err := updater.Update(v)
-	if err != nil {
-		return nil, err
-	}
-	if err = bucket.Put([]byte(key), data); err != nil {
-		return nil, errors.Wrap(err, "writing to db")
-	}
-	return data, nil
-}
diff --git a/pkg/storage/encrypt.go b/pkg/storage/encrypt.go
new file mode 100644
index 0000000..006f6ac
--- /dev/null
+++ b/pkg/storage/encrypt.go
@@ -0,0 +1,151 @@
+package storage
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/pkg/encryption"
+)
+
+type EncryptedWrapper struct {
+	s         ServiceStorage
+	encrypter encryption.Encrypter
+	decrypter encryption.Decrypter
+}
+
+func NewEncryptedWrapper(s ServiceStorage, encrypter encryption.Encrypter, decrypter encryption.Decrypter) *EncryptedWrapper {
+	return &EncryptedWrapper{
+		s:         s,
+		encrypter: encrypter,
+		decrypter: decrypter,
+	}
+}
+
+func (e EncryptedWrapper) Init(opts ...Option) error {
+	return e.s.Init(opts...)
+}
+
+func (e EncryptedWrapper) Type() Type {
+	return e.s.Type()
+}
+
+func (e EncryptedWrapper) URI() string {
+	return e.s.URI()
+}
+
+func (e EncryptedWrapper) IsOpen() bool {
+	return e.s.IsOpen()
+}
+
+func (e EncryptedWrapper) Close() error {
+	return e.s.Close()
+}
+
+func (e EncryptedWrapper) Write(ctx context.Context, namespace, key string, value []byte) error {
+	encryptedData, err := e.encrypter.Encrypt(ctx, value, nil)
+	if err != nil {
+		return errors.Wrap(err, "encrypting data")
+	}
+	return e.s.Write(ctx, namespace, key, encryptedData)
+}
+
+func (e EncryptedWrapper) WriteMany(ctx context.Context, namespace, keys []string, values [][]byte) error {
+	encryptedValues := make([][]byte, 0, len(values))
+	for _, value := range values {
+		encryptedData, err := e.encrypter.Encrypt(ctx, value, nil)
+		if err != nil {
+			return errors.Wrap(err, "encrypting data")
+		}
+		encryptedValues = append(encryptedValues, encryptedData)
+	}
+	return e.s.WriteMany(ctx, namespace, keys, encryptedValues)
+}
+
+func (e EncryptedWrapper) Read(ctx context.Context, namespace, key string) ([]byte, error) {
+	storedBytes, err := e.s.Read(ctx, namespace, key)
+	if err != nil {
+		return nil, err
+	}
+	decryptedData, err := e.decrypter.Decrypt(ctx, storedBytes, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, "decrypting data")
+	}
+	return decryptedData, nil
+}
+
+func (e EncryptedWrapper) Exists(ctx context.Context, namespace, key string) (bool, error) {
+	return e.s.Exists(ctx, namespace, key)
+}
+
+func (e EncryptedWrapper) ReadAll(ctx context.Context, namespace string) (map[string][]byte, error) {
+	encryptedKeyedBytes, err := e.s.ReadAll(ctx, namespace)
+	if err != nil {
+		return nil, err
+	}
+	return e.decryptMap(ctx, encryptedKeyedBytes)
+}
+
+func (e EncryptedWrapper) decryptMap(ctx context.Context, encryptedKeyedBytes map[string][]byte) (map[string][]byte, error) {
+	decryptedValues := make(map[string][]byte, len(encryptedKeyedBytes))
+	for key, encryptedBytes := range encryptedKeyedBytes {
+		decryptedData, err := e.decrypter.Decrypt(ctx, encryptedBytes, nil)
+		if err != nil {
+			return nil, errors.Wrap(err, "decrypting data")
+		}
+		decryptedValues[key] = decryptedData
+	}
+	return decryptedValues, nil
+}
+
+func (e EncryptedWrapper) ReadPage(ctx context.Context, namespace string, pageToken string, pageSize int) (results map[string][]byte, nextPageToken string, err error) {
+	encryptedResults, nextPageToken, err := e.s.ReadPage(ctx, namespace, pageToken, pageSize)
+	if err != nil {
+		return nil, "", err
+	}
+	decryptedMap, err := e.decryptMap(ctx, encryptedResults)
+	if err != nil {
+		return nil, "", err
+	}
+	return decryptedMap, nextPageToken, err
+}
+
+func (e EncryptedWrapper) ReadPrefix(ctx context.Context, namespace, prefix string) (map[string][]byte, error) {
+	encryptedMap, err := e.s.ReadPrefix(ctx, namespace, prefix)
+	if err != nil {
+		return nil, err
+	}
+	return e.decryptMap(ctx, encryptedMap)
+}
+
+func (e EncryptedWrapper) ReadAllKeys(ctx context.Context, namespace string) ([]string, error) {
+	return e.s.ReadAllKeys(ctx, namespace)
+}
+
+func (e EncryptedWrapper) Delete(ctx context.Context, namespace, key string) error {
+	return e.s.Delete(ctx, namespace, key)
+}
+
+func (e EncryptedWrapper) DeleteNamespace(ctx context.Context, namespace string) error {
+	return e.s.DeleteNamespace(ctx, namespace)
+}
+
+type encryptedTx struct {
+	tx        Tx
+	encrypter encryption.Encrypter
+}
+
+func (m encryptedTx) Write(ctx context.Context, namespace, key string, value []byte) error {
+	encryptedData, err := m.encrypter.Encrypt(ctx, value, nil)
+	if err != nil {
+		return errors.Wrap(err, "encrypting data")
+	}
+	return m.tx.Write(ctx, namespace, key, encryptedData)
+}
+
+func (e EncryptedWrapper) Execute(ctx context.Context, businessLogicFunc BusinessLogicFunc, watchKeys []WatchKey) (any, error) {
+	return e.s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) {
+		return businessLogicFunc(ctx, encryptedTx{tx: tx, encrypter: e.encrypter})
+	}, watchKeys)
+}
+
+var _ ServiceStorage = (*EncryptedWrapper)(nil)
diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go
index 6674a60..89131e0 100644
--- a/pkg/storage/storage.go
+++ b/pkg/storage/storage.go
@@ -6,6 +6,7 @@ import (
 	"reflect"
 	"strings"
 
+	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
 
@@ -71,8 +72,6 @@ type ServiceStorage interface {
 	ReadAllKeys(ctx context.Context, namespace string) ([]string, error)
 	Delete(ctx context.Context, namespace, key string) error
 	DeleteNamespace(ctx context.Context, namespace string) error
-	Update(ctx context.Context, namespace string, key string, values map[string]any) ([]byte, error)
-	UpdateValueAndOperation(ctx context.Context, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error)
 	Execute(ctx context.Context, businessLogicFunc BusinessLogicFunc, watchKeys []WatchKey) (any, error)
 }
 
@@ -137,3 +136,74 @@ func Join(parts ...string) string {
 func MakeNamespace(ns ...string) string {
 	return strings.Join(ns, "-")
 }
+
+// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map.
+// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true.
+func UpdateValueAndOperation(ctx context.Context, s ServiceStorage, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) {
+	type pair struct {
+		first  []byte
+		second []byte
+	}
+	watchKeys := []WatchKey{
+		{
+			Namespace: namespace,
+			Key:       key,
+		},
+		{
+			Namespace: opNamespace,
+			Key:       opKey,
+		},
+	}
+	exec, err := s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) {
+		first, err = update(ctx, s, tx, namespace, key, updater)
+		if err != nil {
+			return nil, err
+		}
+		opUpdater.SetUpdatedResponse(first)
+		op, err = update(ctx, s, tx, opNamespace, opKey, opUpdater)
+		if err != nil {
+			return nil, err
+		}
+		return &pair{first: first, second: op}, err
+	}, watchKeys)
+	if err != nil {
+		return nil, nil, err
+	}
+	execPair := exec.(*pair)
+	return execPair.first, execPair.second, nil
+}
+
+func Update(ctx context.Context, s ServiceStorage, namespace, key string, m map[string]any) ([]byte, error) {
+	watchKeys := []WatchKey{
+		{
+			Namespace: namespace,
+			Key:       key,
+		},
+	}
+	exec, err := s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) {
+		return update(ctx, s, tx, namespace, key, NewUpdater(m))
+	}, watchKeys)
+	if err != nil {
+		return nil, err
+	}
+	execBytes := exec.([]byte)
+	return execBytes, nil
+}
+
+func update(ctx context.Context, s ServiceStorage, tx Tx, namespace, key string, updater Updater) ([]byte, error) {
+	readData, err := s.Read(ctx, namespace, key)
+	if err != nil {
+		return nil, err
+	}
+	if err = updater.Validate(readData); err != nil {
+		return nil, errors.Wrap(err, "validating update")
+	}
+	updatedData, err := updater.Update(readData)
+	if err != nil {
+		return nil, err
+	}
+	if err = tx.Write(ctx, namespace, key, updatedData); err != nil {
+		return nil, errors.Wrap(err, "writing to db")
+	}
+	return updatedData, nil
+}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 12cefe6..d629f94 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1,20 +1,32 @@
 package config
 
 import (
+	"embed"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 )
 
-func TestConfig(t *testing.T) {
-	config, err := LoadConfig(Filename)
-	assert.NoError(t, err)
-	assert.NotEmpty(t, config)
+//go:embed testdata
+var testdata embed.FS
 
-	assert.False(t, config.Server.ReadTimeout.String() == "")
-	assert.False(t, config.Server.WriteTimeout.String() == "")
-	assert.False(t, config.Server.ShutdownTimeout.String() == "")
-	assert.False(t, config.Server.APIHost == "")
+func TestLoadConfig(t *testing.T) {
+	t.Run("returns no errors when passed in file", func(t *testing.T) {
+		config, err := LoadConfig(Filename, nil)
+		assert.NoError(t, err)
+		assert.NotEmpty(t, config)
 
-	assert.NotEmpty(t, config.Services.StorageProvider)
+		assert.False(t, config.Server.ReadTimeout.String() == "")
+		assert.False(t, config.Server.WriteTimeout.String() == "")
+		assert.False(t, config.Server.ShutdownTimeout.String() == "")
+		assert.False(t, config.Server.APIHost == "")
+
+		assert.NotEmpty(t, config.Services.StorageProvider)
+	})
+
+	t.Run("returns errors when prod disables encryption", func(t *testing.T) {
+		_, err := LoadConfig("testdata/test1.toml", testdata)
+		assert.Error(t, err)
+		assert.ErrorContains(t, err, "prod environment cannot disable key encryption")
+	})
 }
diff --git a/config/test.toml b/config/test.toml
index 141c6a4..02399cc 100644
--- a/config/test.toml
+++ b/config/test.toml
@@ -25,6 +25,11 @@ log_level = "warn"
 enable_schema_caching = true
 enable_allow_all_cors = true
 
+[services.storage_encryption]
+# master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*"
+# kms_credentials_path = "credentials.json"
+disable_encryption = false
+
 # Storage Configuration
 [services]
 service_endpoint = "http://localhost:8080"
@@ -43,6 +48,7 @@ option = "password"
 name = "keystore"
 # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*"
 # kms_credentials_path = "credentials.json"
+disable_encryption = false
 
 [services.did]
 name = "did"
diff --git a/config/testdata/test1.toml b/config/testdata/test1.toml
new file mode 100644
index 0000000..f96f219
--- /dev/null
+++ b/config/testdata/test1.toml
@@ -0,0 +1,6 @@
+[server]
+env = "prod" # either 'dev', 'test', or 'prod'
+
+[services.keystore]
+name = "keystore"
+disable_encryption = true
\ No newline at end of file
diff --git a/pkg/encryption/encryption_test.go b/pkg/encryption/encryption_test.go
new file mode 100644
index 0000000..ad1eddb
--- /dev/null
+++ b/pkg/encryption/encryption_test.go
@@ -0,0 +1,93 @@
+package encryption
+
+import (
+	"context"
+	"testing"
+
+	"github.com/TBD54566975/ssi-sdk/crypto"
+	sdkutil "github.com/TBD54566975/ssi-sdk/util"
+	"github.com/mr-tron/base58"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/tbd54566975/ssi-service/internal/util"
+	"golang.org/x/crypto/chacha20poly1305"
+)
+
+func createServiceKey() (key string, err error) {
+	keyBytes, err := util.GenerateSalt(chacha20poly1305.KeySize)
+	if err != nil {
+		err = errors.Wrap(err, "generating bytes for service key")
+		return "", sdkutil.LoggingError(err)
+	}
+
+	key = base58.Encode(keyBytes)
+	return
+}
+
+func TestEncryptDecryptAllKeyTypes(t *testing.T) {
+	serviceKeyEncoded, err := createServiceKey()
+	assert.NoError(t, err)
+	serviceKey, err := base58.Decode(serviceKeyEncoded)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, serviceKey)
+	encrypter := NewXChaCha20Poly1305EncrypterWithKeyResolver(func(ctx context.Context) ([]byte, error) {
+		return serviceKey, nil
+	})
+
+	tests := []struct {
+		kt crypto.KeyType
+	}{
+		{
+			kt: crypto.Ed25519,
+		},
+		{
+			kt: crypto.X25519,
+		},
+		{
+			kt: crypto.SECP256k1,
+		},
+		{
+			kt: crypto.P224,
+		},
+		{
+			kt: crypto.P256,
+		},
+		{
+			kt: crypto.P384,
+		},
+		{
+			kt: crypto.P521,
+		},
+		{
+			kt: crypto.RSA,
+		},
+	}
+	for _, test := range tests {
+		t.Run(string(test.kt), func(t *testing.T) {
+			// generate a new key based on the given key type
+			_, privKey, err := crypto.GenerateKeyByKeyType(test.kt)
+			assert.NoError(t, err)
+			assert.NotEmpty(t, privKey)
+
+			// serialize the key before encryption
+			privKeyBytes, err := crypto.PrivKeyToBytes(privKey)
+			assert.NoError(t, err)
+			assert.NotEmpty(t, privKeyBytes)
+
+			// encrypt the serviceKey using our service serviceKey
+			encryptedKey, err := encrypter.Encrypt(context.Background(), privKeyBytes, nil)
+			assert.NoError(t, err)
+			assert.NotEmpty(t, encryptedKey)
+
+			// decrypt the serviceKey using our service serviceKey
+			decryptedKey, err := encrypter.Decrypt(context.Background(), encryptedKey, nil)
+			assert.NoError(t, err)
+			assert.NotEmpty(t, decryptedKey)
+
+			// reconstruct the key from its serialized form
+			privKeyReconstructed, err := crypto.BytesToPrivKey(decryptedKey, test.kt)
+			assert.NoError(t, err)
+			assert.EqualValues(t, privKey, privKeyReconstructed)
+		})
+	}
+}
diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index 8952ad9..97d25c8 100644
--- a/pkg/server/router/did_test.go
+++ b/pkg/server/router/did_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
 	"github.com/tbd54566975/ssi-service/pkg/testutil"
+	"gopkg.in/h2non/gock.v1"
 
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/service/did"
@@ -189,12 +190,22 @@ func TestDIDRouter(t *testing.T) {
 
 				assert.ElementsMatch(tt, supported.Methods, []didsdk.Method{didsdk.KeyMethod, didsdk.WebMethod})
 
+				gock.Off()
+				gock.New("https://example.com").
+					Get("/.well-known/did.json").
+					Reply(200).
+					BodyString("")
 				// bad key type
 				createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"}
 				_, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts})
 				assert.Error(tt, err)
 				assert.Contains(tt, err.Error(), "could not generate key for did:web")
 
+				gock.Off()
+				gock.New("https://example.com").
+					Get("/.well-known/did.json").
+					Reply(200).
+					BodyString("")
 				// good key type
 				createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts})
 				assert.NoError(tt, err)
@@ -211,6 +222,11 @@ func TestDIDRouter(t *testing.T) {
 				// make sure it's the same value
 				assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID)
 
+				gock.Off()
+				gock.New("https://tbd.website").
+					Get("/.well-known/did.json").
+					Reply(200).
+					BodyString("")
 				// create a second DID
 				createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"}
 				createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts})
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 2b1e89c..dca596f 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
 	"github.com/gin-gonic/gin"
-
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/service/issuance"
 	"github.com/tbd54566975/ssi-service/pkg/service/manifest/model"
@@ -49,7 +48,7 @@ func TestMain(t *testing.M) {
 
 func TestHealthCheckAPI(t *testing.T) {
 	shutdown := make(chan os.Signal, 1)
-	serviceConfig, err := config.LoadConfig("")
+	serviceConfig, err := config.LoadConfig("", nil)
 	assert.NoError(t, err)
 	server, err := NewSSIServer(shutdown, *serviceConfig)
 	assert.NoError(t, err)
@@ -77,7 +76,7 @@ func TestReadinessAPI(t *testing.T) {
 	})
 
 	shutdown := make(chan os.Signal, 1)
-	serviceConfig, err := config.LoadConfig("")
+	serviceConfig, err := config.LoadConfig("", nil)
 	assert.NoError(t, err)
 	serviceConfig.Services.StorageOptions = []storage.Option{
 		{
@@ -230,8 +229,9 @@ func testKeyStoreService(t *testing.T, db storage.ServiceStorage) (*keystore.Ser
 	}
 
 	// create a keystore service
-	require.NoError(t, keystore.EnsureServiceKeyExists(serviceConfig, db))
-	factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db)
+	encrypter, decrypter, err := keystore.NewServiceEncryption(db, serviceConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey)
+	require.NoError(t, err)
+	factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db, encrypter, decrypter)
 	keystoreService, err := factory(db)
 	require.NoError(t, err)
 	require.NotEmpty(t, keystoreService)
diff --git a/pkg/server/server_webhook_test.go b/pkg/server/server_webhook_test.go
index ec6f153..09d7229 100644
--- a/pkg/server/server_webhook_test.go
+++ b/pkg/server/server_webhook_test.go
@@ -48,7 +48,7 @@ func TestSimpleWebhook(t *testing.T) {
 	defer testServer.Close()
 
 	shutdown := make(chan os.Signal, 1)
-	serviceConfig, err := config.LoadConfig("")
+	serviceConfig, err := config.LoadConfig("", nil)
 	assert.NoError(t, err)
 
 	serviceConfig.Server.APIHost = "0.0.0.0:" + freePort()
diff --git a/pkg/service/keystore/service_test.go b/pkg/service/keystore/service_test.go
index 16c28ba..7adf5ea 100644
--- a/pkg/service/keystore/service_test.go
+++ b/pkg/service/keystore/service_test.go
@@ -12,7 +12,6 @@ import (
 	"github.com/mr-tron/base58"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 )
@@ -23,71 +22,6 @@ func TestGenerateServiceKey(t *testing.T) {
 	assert.NotEmpty(t, key)
 }
 
-func TestEncryptDecryptAllKeyTypes(t *testing.T) {
-	serviceKeyEncoded, err := GenerateServiceKey()
-	assert.NoError(t, err)
-	serviceKey, err := base58.Decode(serviceKeyEncoded)
-	assert.NoError(t, err)
-	assert.NotEmpty(t, serviceKey)
-
-	tests := []struct {
-		kt crypto.KeyType
-	}{
-		{
-			kt: crypto.Ed25519,
-		},
-		{
-			kt: crypto.X25519,
-		},
-		{
-			kt: crypto.SECP256k1,
-		},
-		{
-			kt: crypto.P224,
-		},
-		{
-			kt: crypto.P256,
-		},
-		{
-			kt: crypto.P384,
-		},
-		{
-			kt: crypto.P521,
-		},
-		{
-			kt: crypto.RSA,
-		},
-	}
-	for _, test := range tests {
-		t.Run(string(test.kt), func(t *testing.T) {
-			// generate a new key based on the given key type
-			_, privKey, err := crypto.GenerateKeyByKeyType(test.kt)
-			assert.NoError(t, err)
-			assert.NotEmpty(t, privKey)
-
-			// serialize the key before encryption
-			privKeyBytes, err := crypto.PrivKeyToBytes(privKey)
-			assert.NoError(t, err)
-			assert.NotEmpty(t, privKeyBytes)
-
-			// encrypt the serviceKey using our service serviceKey
-			encryptedKey, err := EncryptKey(serviceKey, privKeyBytes)
-			assert.NoError(t, err)
-			assert.NotEmpty(t, encryptedKey)
-
-			// decrypt the serviceKey using our service serviceKey
-			decryptedKey, err := DecryptKey(serviceKey, encryptedKey)
-			assert.NoError(t, err)
-			assert.NotEmpty(t, decryptedKey)
-
-			// reconstruct the key from its serialized form
-			privKeyReconstructed, err := crypto.BytesToPrivKey(decryptedKey, test.kt)
-			assert.NoError(t, err)
-			assert.EqualValues(t, privKey, privKeyReconstructed)
-		})
-	}
-}
-
 func TestStoreAndGetKey(t *testing.T) {
 	keyStore, err := createKeyStoreService(t)
 	assert.NoError(t, err)
diff --git a/pkg/storage/db_test.go b/pkg/storage/db_test.go
index 5c8980b..17151c9 100644
--- a/pkg/storage/db_test.go
+++ b/pkg/storage/db_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/goccy/go-json"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/tbd54566975/ssi-service/pkg/encryption"
 )
 
 func getDBImplementations(t *testing.T) []ServiceStorage {
@@ -29,6 +30,13 @@ func getDBImplementations(t *testing.T) []ServiceStorage {
 	postgresDB := setupPostgresDB(t)
 	dbImpls = append(dbImpls, postgresDB)
 
+	key := make([]byte, 32)
+	dbImpls = append(dbImpls, NewEncryptedWrapper(
+		boltDB,
+		encryption.NewXChaCha20Poly1305EncrypterWithKey(key),
+		encryption.NewXChaCha20Poly1305EncrypterWithKey(key),
+	))
+
 	return dbImpls
 }
 
@@ -498,7 +506,7 @@ func TestDB_Update(t *testing.T) {
 		}
 		for _, tt := range tests {
 			t.Run(tt.name, func(t *testing.T) {
-				data, err = db.Update(context.Background(), namespace, tt.args.key, tt.args.values)
+				data, err = Update(context.Background(), db, namespace, tt.args.key, tt.args.values)
 				if !tt.expectedError(t, err) {
 					return
 				}
@@ -520,6 +528,19 @@ func (f testOpUpdater) SetUpdatedResponse(bytes []byte) {
 	f.UpdaterWithMap.Values["response"] = bytes
 }
 
+func TestDB_Execute(t *testing.T) {
+	for _, dbImpl := range getDBImplementations(t) {
+		db := dbImpl
+		_, err := db.Execute(context.Background(), func(ctx context.Context, tx Tx) (any, error) {
+			return nil, tx.Write(ctx, "hello", "my_key", []byte(`some bytes`))
+		}, nil)
+		assert.NoError(t, err)
+		result, err := db.Read(context.Background(), "hello", "my_key")
+		assert.NoError(t, err)
+		assert.Equal(t, []byte(`some bytes`), result)
+	}
+}
+
 func TestDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) {
 	for _, dbImpl := range getDBImplementations(t) {
 		db := dbImpl
@@ -609,7 +630,7 @@ func TestDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) {
 		}
 		for _, tt := range tests {
 			t.Run(tt.name, func(t *testing.T) {
-				gotFirstData, gotOpData, err := db.UpdateValueAndOperation(context.Background(), tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{
+				gotFirstData, gotOpData, err := UpdateValueAndOperation(context.Background(), db, tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{
 					NewUpdater(map[string]any{
 						"done": true,
 					}),