Encrypt smarter, not harder: The Basis Theory Open Source KMS SDK
When we started designing Basis Theory's vault, we knew the platform encryption posture would need to change to meet new security, compliance, and customer requirements dynamically. As a result, we understood that encryption would be at the very core of our product. Today, we are proud to share that core to everyone by introducing Basis Theory’s Open KMS SDK.
In this blog, we’ll take a look at some of the key features of the new tool, but check out our workshop from Fintech Devcon and repo with working code samples to learn more.
Combining Ciphertext and Key Metadata
Most existing encryption solutions keep the ciphertext and key management as separate concerns. A major problem with this approach is that it makes your encryption implementation static. For instance, changing the encryption key or algorithm requires re-encrypting all data. As we evaluated options to allow dynamic encryption keys, algorithms, key sizes, and key rotation policies, we realized we needed a solution that combined the ciphertext and key metadata needed for key management.
JSON Web Encryption provides a standard that combines ciphertext and key management, enabling many security and compliance requirements such as key rotation, data residency, and support for multiple KMS solutions. JWE defines a format for storing the ciphertext for encryption and wrapping operations and leverages JSON Web Keys (JWK) to define a format of the encryption key data such as algorithm, key size, key identifier, and key location.
Schema Registration Support
Our goal was to centralize the configuration and management of all encryption information via schema registration. Doing so enables a single place to go to show the configuration of your encryption keys, algorithms, and key sizes to assist with auditing your application’s security posture.
In order to register a new encryption scheme, we simply add the following lines to our application startup code:
services.AddEncryption(o =>
{
o.DefaultScheme = "default";
})
.AddScheme("default", builder =>
{
builder.AddAesContentEncryption(handlerOptions =>
{
handlerOptions.EncryptionAlgorithm = EncryptionAlgorithm.A256CBC_HS512;
handlerOptions.KeySize = 256;
});
});
Now, we can easily encrypt any data in our code via the Encryption Service:
public class MyAwesomeService()
{
private readonly IEncryptionService _encryptionService;
public MyAwesomeService(IEncryptionService encryptionService) => _encryptionService = encryptionService;
public async Task DoAwesomeStuff()
{
var encrypted = await _encryptionService.EncryptAsync(“My Secret”, "default");
}
}
This gives us the ability to centrally manage all of our encryption keys and configuration in one spot without having to change any of our production code.
Implementations for Major KMS Providers
Providing out-of-the-box KMS support for major cloud providers is important to get quickly up and running while following best practices with key management. Each KMS provider offers its SDK and implementation for generating keys, performing encrypt and decrypt operations, and handling key rotation. While this can save time, it can often leave engineers researching how to solve these problems and questioning if their implementation is correct.
We wanted to remove this burden by providing a standard implementation for all major cloud providers. Today we support Azure and AWS, with Google Cloud coming soon.
Adding support for popular KMS providers is as simple as registering the encryption provider:
services.AddEncryption(o =>
{
o.DefaultScheme = "Azure Provider";
})
.AddScheme("Azure Provider", builder =>
{
builder.AddKeyVaultContentEncryption(handlerOptions =>
{
handlerOptions.KeyName = configuration.GetValue<string>("Encryption:KeyName");
handlerOptions.EncryptionAlgorithm = EncryptionAlgorithm.RSA_OAEP;
handlerOptions.KeySize = 4096;
});
});
Support for Key Rotation
Each KMS provider supports key rotation via different approaches.
AWS allows specifying a key rotation policy on the key, while Azure requires setting an expiration date on the version of the key and generating a new version of the key. Since each KMS provider implements this differently, we aimed to standardize this. To accomplish this with our Open KMS SDK, we just need to specify the key rotation timespan we want a key to be valid for, and the SDK will automatically handle the key rotation for us:
builder.AddKeyVaultKeyEncryption(handlerOptions =>
{
handlerOptions.KeyRotationInterval = TimeSpan.FromDays(30)
});
In the above example, we will rotate the encryption key automatically every 30 days.
Support for Dependency Injection
Key management isn’t always static. For example, you must often deal with multi-tenant environments with per-user or per-customer encryption keys. To support this, we offer dependency injection when registering a new schema.
options.AddKeyVaultKeyEncryption<HttpContext>((handlerOptions, httpContext) =>
{
handlerOptions.KeyName = httpContext.User.Identity.Name;
});
In this example, we are registering the Azure KeyVault provider and dynamically setting the encryption key to be the name of the currently logged-in user. This enables us to use per-user encryption keys to ensure that each user’s data is encrypted with a unique key that keeps their data isolated from other users’ data.
Object Relational Mapper (ORM) Support
Explicitly calling “encrypt” and “decrypt” on data may not always be optimal for engineers. It requires multiple touch points in applications every time you read and write sensitive data. To simplify this, we added support for Entity Framework to automatically handle the secure lifecycle of our data.
To add the Open KMS SDK to our DBContext, we can simply inject the Encryption Service and register it:
public class BankDbContext : DbContext
{
private readonly IEncryptionService _encryptionService;
public DbSet<Bank> Banks { get; set; } = default!;
public BankDbContext (DbContextOptions<BankDbContext> options, IEncryptionService encryptionService) : base(options)
{
_encryptionService = encryptionService;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseEncryption(_encryptionService);
modelBuilder.Entity<Bank>(Bank.Configure);
}
}
Next, we can annotate our data model, specifying the encryption schemas we want to use for each property:
public class Bank
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
[Required, Encrypted(EncryptionSchemes.BankRoutingNumber)]
public string RoutingNumber { get; set; }
[Required, Encrypted(EncryptionSchemes.BankAccountNumber)]
public string AccountNumber { get; set; }
}
Whenever we interact with our Bank entity, the data will automatically be encrypted and decrypted for us:
var bank = await dbContext.Banks.FirstOrDefaultAsync(m => m.Id == id);
var decryptedRoutingNumber = bank.RountingNumber;
var decryptedAccountNumber = bank.AccountNumber;
And that’s it! We can use two different encryption schema policies for our routing and account number on our bank model and not have to explicitly call the Encryption Service anywhere to encrypt and decrypt data explicitly. This reduces the cognitive load on engineers and the potential risk of forgetting to handle sensitive data.
Next steps
Solving for encryption and key management was the first problem we set out to solve at Basis Theory. Our Open KMS SDK enables a wide range of security, compliance, and customer requirements.
We have plans to support more KMS providers, a wider variety of encryption algorithms, and support more programming languages and frameworks. If you have feedback, feature requests, or additional requirements, reach out to us in our Slack community or send us a ticket on GitHub.
Looking to use your secured data? Check out our developer docs to learn more about our tokenization platform and how we can remove sensitive data from your systems without losing its utility to developers.