Skip to content

Latest commit

 

History

History

README.md

Asherah - C#

Application level envelope encryption SDK for C# with support for cloud-agnostic data storage and key management.

Version

Installation

You can get the latest releases from NuGet.

<ItemGroup>
    <PackageReference Include="GoDaddy.Asherah.AppEncryption" Version="0.9.0" />
    <PackageReference Include="GoDaddy.Asherah.AppEncryption.PlugIns.Aws" Version="0.9.0" />
</ItemGroup>

Our libraries currently target netstandard2.0, net8.0, net9.0.

Quick Start

// Create a session factory. The builder steps used below are for testing only.
var staticKeyManagementService = new StaticKeyManagementServiceImpl("thisIsAStaticMasterKeyForTestingOnly");

using (SessionFactory sessionFactory = SessionFactory
    .NewBuilder("some_product", "some_service")
    .WithMemoryPersistence()
    .WithNeverExpiredCryptoPolicy()
    .WithKeyManagementService(staticKeyManagementService)
    .Build())
{
    // Now create a cryptographic session for a partition.
    using (Session<byte[], byte[]> sessionBytes =
        sessionFactory.GetSessionBytes("some_partition"))
    {
        // Encrypt some data
        const string originalPayloadString = "mysupersecretpayload";
        byte[] dataRowRecordBytes = sessionBytes.Encrypt(Encoding.UTF8.GetBytes(originalPayloadString));

        // Decrypt the data
        string decryptedPayloadString = Encoding.UTF8.GetString(sessionBytes.Decrypt(dataRowRecordBytes));
    }
}

A more extensive example is the Reference Application, which will evolve along with the SDK.

How to Use Asherah

Before you can start encrypting data, you need to define Asherah's required pluggable components. Below we show how to build the various options for each component.

Define the Metastore

Detailed information about the Metastore, including any provisioning steps, can be found here.

RDBMS Metastore

Asherah can connect to a relational database by accepting an ADO DbProviderFactory and a connection string.

// Create / retrieve a DbProviderFactory for your target vendor, as well as the connection string
DbProviderFactory dbProviderFactory = ...;
string connectionString = ...;

// Build the ADO Metastore
IMetastore<JObject> adoMetastore = AdoMetastoreImpl.NewBuilder(dbProviderFactory, connectionString).Build();

DynamoDB Metastore

For simplicity, the DynamoDB implementation uses the builder pattern to enable configuration changes.

To obtain an instance of the builder, use the static factory method NewBuilder.

DynamoDbMetastoreImpl.NewBuilder("<preferred-aws-region>");

Once you have a builder, you can either use the WithXXX setter methods to configure the metastore properties or simply build the metastore by calling the Build method.

  • WithKeySuffix: Specifies whether key suffix should be enabled for DynamoDB. This is required to enable Global Tables.
  • WithTableName: Specifies the name of the DynamoDb table.
  • WithRegion: Specifies the region for the AWS DynamoDb client. Will be ignored if WithEndPointConfiguration was called first.
  • WithEndPointConfiguration: Adds an EndPoint configuration to the AWS DynamoDb client. Will be ignored if WithRegion was called first.
  • WithCredentials: Specifies custom credentials for the AWS DynamoDb client.
  • WithDynamoDbClient: Recommended - Provides a custom DynamoDB client. This method is preferred over letting the builder create the client, especially when using dependency injection frameworks. It gives you full control over client configuration and lifecycle management. Completely bypasses all other client configuration methods.

Below is an example of a DynamoDB metastore that uses a Global Table named TestTable

// Setup region via global default or via other AWS .NET SDK mechanisms
AWSConfigs.AWSRegion = "us-west-2";

// Build the DynamoDB Metastore.
IMetastore<JObject> dynamoDbMetastore = DynamoDbMetastoreImpl.NewBuilder("us-west-2")
      .WithKeySuffix()
      .WithTableName("TestTable")
      .Build();

Recommended: Using WithDynamoDbClient with IServicesCollection and AWSSDK.Extensions.NETCore.Setup

// In your DI container setup (e.g., Startup.cs, Program.cs)
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
services.AddAWSService<IAmazonDynamoDB>();

// In your service or controller
public class AsherahHelper(IAmazonDynamoDB dynamoDbClient, AWSOptions awsOptions)
{
    public SessionFactory BuildSessionFactory()
    {
        var preferredRegion = awsOptions.Region.SystemName;
        var metastore = DynamoDbMetastoreImpl.NewBuilder(preferredRegion)
            .WithDynamoDbClient(dynamoDbClient)  // Use the injected client
            .WithKeySuffix()
            .WithTableName("TestTable")
            .Build();

        // continue building factory here
    }
}

Alternative: Manual Client Configuration

// Create a custom DynamoDB client with specific configuration
var config = new AmazonDynamoDBConfig
{
    RegionEndpoint = Amazon.RegionEndpoint.USWest2,
    Timeout = TimeSpan.FromSeconds(30)
};
var customClient = new AmazonDynamoDBClient(credentials, config);

// Use the custom client in the metastore
IMetastore<JObject> dynamoDbMetastore = DynamoDbMetastoreImpl.NewBuilder("us-west-2")
    .WithDynamoDbClient(customClient)
    .WithKeySuffix()
    .WithTableName("TestTable")
    .Build();

In-memory Metastore (FOR TESTING ONLY)

IMetastore<JObject> metastore = new InMemoryPersistenceImpl<JObject>();

Define the Key Management Service

Detailed information about the Key Management Service can be found here.

AWS KMS

Note

This section now covers using the recommended GoDaddy.Asherah.AppEncryption.PlugIns.Aws.Kms.KeyManagementService The GoDaddy.Asherah.AppEncryption.Kms.AwsKeyManagementServiceImpl is obsolete and will be removed in the future. See the Plugins Upgrade Guide for migration instructions.

One way to create your Key Management Service is to use the builder, use the static factory method NewBuilder. Provide an ILoggerFactory, your region key arns and AWS credentials. A good strategy if using multiple regions is to provide the closest regions first based on what region your app is running in.

var keyManagementService = KeyManagementService.NewBuilder()
  .WithLoggerFactory(myLoggerFactory) // required
  .WithRegionKeyArn("us-east-1", "arn:aws:kms:us-east-1:123456789012:key/abc") // add these in priority order
  .WithRegionKeyArn("us-west-2", "arn:aws:kms:us-west-2:234567890123:key/def")
  .WithCredentials(myAwsCredentials)
  .Build()

Other options when using the builder are:

  • WithOptions: Provide a KeyManagementServiceOptions that contains the RegionKeyArns instead of WithRegionKeyArn.
  • WithKmsClientFactory: Provide your own client factory instead of using WithCredentials.

The KeyManagementServiceOptions is easily deserializable from appsettings/configuration You can implement your own IKeyManagementClientFactory if you need better control how your AWS Kms Clients are created.

Recommended: Setting up with IServicesCollection and AWSSDK.Extensions.NETCore.Setup

{
  "AsherahKmsOptions": {
    "regionKeyArns": [
      {
        "region": "us-west-2",
        "keyArn": "Key Arn for us-west-2"
      },
      {
        "region": "us-east-1",
        "keyArn": "Key Arn for us-east-1"
      }
    ]
  }
}

Then, in code:

// In your DI container setup (e.g., Startup.cs, Program.cs)
// assumes you also have setup ILoggerFactory
services.AddDefaultAWSOptions(Configuration.GetAWSOptions());

var kmsOptions = Configuration.GetValue<KeyManagementServiceOptions>("AsherahKmsOptions");
services.AddSingleton(kmsOptions);

// Then later in a class
public class MyService(AWSOptions awsOptions, KeyManagementServiceOptions kmsOptions, ILoggerFactory loggerFactory)
{
  public IKeyManagementService CreateKeyManagementServiceForAsherah(){

    // Optimize KMS options to prioritize the current AWS region
    // This is optional.  If your kmsOptions are not configured by region, you would use this
    // to do a runtime sort based on the current running region.
    // Note: this example is simply putting the current region first and leaving the sequence of the
    // remaining in the order they were in.
    var optimizedKmsOptions = kmsOptions.OptimizeByRegions(awsOptions.Region.SystemName);

    var keyManagementService = KeyManagementService.NewBuilder()
      .WithLoggerFactory(loggerFactory)
      .WithOptions(optimizedKmsOptions)
      .WithCredentials(awsOptions.GetCredentials())
      .Build();

    // return the keyManagementService that can be passed into the SessionFactory builder
    return keyManagementService;
  }

}

Static KMS (FOR TESTING ONLY)

KeyManagementService keyManagementService = new StaticKeyManagementServiceImpl("thisIsAStaticMasterKeyForTesting");

Define the Crypto Policy

Detailed information on Crypto Policy can be found here. The Crypto Policy's effect on key caching is explained here.

Basic Expiring Crypto Policy

CryptoPolicy cryptoPolicy = BasicExpiringCryptoPolicy.NewBuilder()
    .WithKeyExpirationDays(90)
    .WithRevokeCheckMinutes(60)
    .Build();

(Optional) Enable Session Caching

Session caching is disabled by default. Enabling it is primarily useful if you are working with stateless workloads and the shared session can't be used by the calling app.

To enable session caching, simply use the optional builder step WithCanCacheSessions(true) when building a crypto policy.

CryptoPolicy cryptoPolicy = BasicExpiringCryptoPolicy.NewBuilder()
    .WithKeyExpirationDays(90)
    .WithRevokeCheckMinutes(60)
    .WithCanCacheSessions(true)    // Enable session cache
    .WithSessionCacheMaxSize(200)    // Define the number of maximum sessions to cache
    .WithSessionCacheExpireMillis(5000)    // Evict the session from cache after some milliseconds
    .Build();

Never Expired Crypto Policy (FOR TESTING ONLY)

CryptoPolicy neverExpiredCryptoPolicy = new NeverExpiredCryptoPolicy();

(Optional) Enable Metrics

Asherah's C# implementation uses App.Metrics for metrics, which are disabled by default. If metrics are left disabled, we simply create and use an IMetricsinstance whose Enabled flag is disabled.

To enable metrics generation, simply pass in an existing IMetrics instance to the final optional builder step when creating a SessionFactory.

The following metrics are available:

  • ael.drr.decrypt: Total time spent on all operations that were needed to decrypt.
  • ael.drr.encrypt: Total time spent on all operations that were needed to encrypt.
  • ael.kms.aws.decrypt.<region>: Time spent on decrypting the region-specific keys.
  • ael.kms.aws.decryptkey: Total time spend in decrypting the key which would include the region-specific decrypt calls in case of transient failures.
  • ael.kms.aws.encrypt.<region>: Time spent on data key plain text encryption for each region.
  • ael.kms.aws.encryptkey: Total time spent in encrypting the key which would include the region-specific generatedDataKey and parallel encrypt calls.
  • ael.kms.aws.generatedatakey.<region>: Time spent to generate the first data key which is then encrypted in remaining regions.
  • ael.metastore.ado.load: Time spent to load a record from ado metastore.
  • ael.metastore.ado.loadlatest: Time spent to get the latest record from ado metastore.
  • ael.metastore.ado.store: Time spent to store a record into ado metastore.
  • ael.metastore.dynamodb.load: Time spent to load a record from DynamoDB metastore.
  • ael.metastore.dynamodb.loadlatest: Time spent to get the latest record from DynamoDB metastore.
  • ael.metastore.dynamodb.store: Time spent to store a record into DynamoDB metastore.

Build a Session Factory

A session factory can now be built using the components we defined above.

SessionFactory sessionFactory = SessionFactory.NewBuilder("some_product", "some_service")
     .WithMetastore(metastore)
     .WithCryptoPolicy(policy)
     .WithKeyManagementService(keyManagementService)
     .WithMetrics(metrics) // Optional
     .Build();

NOTE: We recommend that every service have its own session factory, preferably as a singleton instance within the service. This will allow you to leverage caching and minimize resource usage. Always remember to close the session factory before exiting the service to ensure that all resources held by the factory, including the cache, are disposed of properly.

Performing Cryptographic Operations

Create a Session session to be used for cryptographic operations.

Session<byte[], byte[]> sessionBytes = sessionFactory.GetSessionBytes("some_user");

The different usage styles are explained below.

NOTE: Remember to close the session after all cryptographic operations to dispose of associated resources.

Plain Encrypt/Decrypt Style

This usage style is similar to common encryption utilities where payloads are simply encrypted and decrypted, and it is completely up to the calling application for storage responsibility.

string originalPayloadString = "mysupersecretpayload";

// encrypt the payload
byte[] dataRowRecordBytes = sessionBytes.Encrypt(Encoding.UTF8.GetBytes(originalPayloadString));

// decrypt the payload
string decryptedPayloadString = Encoding.UTF8.GetString(dataRowRecordBytes.Decrypt(newDataRowRecordBytes));

Custom Persistence via Store/Load methods

Asherah supports a key-value/document storage model. An AppEncryption instance can accept a Persistence implementation and hook into its Load and Store calls.

An example Dictionary-backed Persistence implementation:

public class DictionaryPersistence : Persistence<JObject>
{
    Dictionary<string, JObject> dictionaryPersistence = new Dictionary<string, JObject>();

    public override Option<JObject> Load(String key)
    {
        return dictionaryPersistence.TryGetValue(key, out JObject result) ? result : Option<JObject>.None;
    }

    public override void Store(String key, JObject value)
    {
        dictionaryPersistence.Add(key, value);
    }
}

An example end-to-end use of the store and load calls:

// Encrypts the payload, stores it in the dictionaryPersistence and returns a look up key
string persistenceKey = sessionJson.Store(originalPayload.ToJObject(), dictionaryPersistence);

// Uses the persistenceKey to look-up the payload in the dictionaryPersistence, decrypts the payload if any and then returns it
Option<JObject> payload = sessionJson.Load(persistenceKey, dictionaryPersistence);

Deployment Notes

Handling read-only Docker containers

Dotnet enables debugging and profiling by default causing filesystem writes. Disabling them ensures that the SDK can be used in a read-only container.

To do so, simply set the environment variable COMPlus_EnableDiagnostics to 0

ENV COMPlus_EnableDiagnostics=0

Our sample application's Dockerfile can be used for reference.

Development Notes

Multi Targeting

Unit Tests

Some unit tests will use the AWS SDK, If you don’t already have a local AWS credentials file, create a dummy file called ~/.aws/credentials with the below contents:

[default]
aws_access_key_id = foobar
aws_secret_access_key = barfoo

Alternately, you can set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.

Regression Tests

The regression test use configuration parameters that can be passed either by using a config.yaml file or by setting the environment variables. The below table outlines the parameter names and their default values (if any)

Config File Environment Variable Default Value
kmsType KMS_TYPE static
kmsAwsRegionArnTuples KMS_AWS_REGION_ARN_TUPLES N/A
kmsAwsPreferredRegion KMS_AWS_PREFERRED_REGION us-west-2
metastoreType METASTORE_TYPE memory
metastoreAdoConnectionString METASTORE_ADO_CONNECTION_STRING N/A