

Using AWS KMS with Laravel
source link: https://blog.deleu.dev/swapping-laravel-encryption-with-aws-kms/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Using AWS KMS with Laravel
March 12, 2021
AWS KMS is an incredible offering by AWS that manages encryption keys, automatic rotation and secure storage. With rotation enabled, AWS will generate a new encryption key once a year without deleting the previous keys. Any cipher generated by old keys can still be decrypted. We don’t have access to the actual key, which means we can’t leak it.
Laravel ships an Encrypter class that uses AES for encryption. Replacing Laravel’s implementation with KMS takes only a simple KmsEncrypter and a Service Provider.
The Service Provider
I just sent out a pull request to introduce a StringEncrypter interface into Laravel so that this process can be simplified. https://github.com/laravel/framework/pull/36578
The Service Provider can look like this:
final class EncryptionServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(KmsEncrypter::class, function () {
$key = config('encryption.key');
$context = config('encryption.context');
$client = $this->app->make(KmsClient::class);
return new KmsEncrypter($client, $key, $context ?? []);
});
$this->app->alias(KmsEncrypter::class, 'encrypter');
$this->app->alias(KmsEncrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class);
$this->app->alias(KmsEncrypter::class, \Illuminate\Contracts\Encryption\StringEncrypter::class);
}
}
Notice how we’re instantiating a KmsClient
inside the KmsEncrypter
factory callback. That gives us the chance of delegating how
we should resolve KmsClient
to a separate process. One way
of doing that could be like this:
final class AwsServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerS3();
$this->registerKms();
}
private function registerS3(): void
{
$this->app->bind(S3Client::class, function () {
$region = config('aws.region');
return new S3Client(['region' => $region, 'version' => '2006-03-01']);
});
}
private function registerKms(): void
{
$this->app->bind(KmsClient::class, fn() => new KmsClient([
'version' => '2014-11-01',
'region' => config('aws.region'),
]));
}
}
KmsEncrypter
Here’s how KmsEncrypter
would look like:
final class KmsEncrypter implements Encrypter, StringEncrypter
{
public function __construct(private KmsClient $client, private string $key, private array $context) {}
public function encrypt($value, $serialize = true)
{
try {
return base64_encode($this->client->encrypt([
'KeyId' => $this->key,
'Plaintext' => $value,
'EncryptionContext' => $this->context,
])->get('CiphertextBlob'));
} catch (AwsException $e) {
throw new EncryptException($e->getMessage(), $e->getCode(), $e);
}
}
public function decrypt($payload, $unserialize = true)
{
try {
$result = $this->client->decrypt([
'CiphertextBlob' => base64_decode($payload),
'EncryptionContext' => $this->context,
]);
return $result['Plaintext'];
} catch (AwsException $e) {
throw new DecryptException($e->getMessage(), $e->getCode(), $e);
}
}
public function encryptString(string $value): string
{
return $this->encrypt($value, false);
}
public function decryptString(string $value): string
{
return $this->decrypt($value, false);
}
}
Let’s dissect this class. First we have the KmsClient
that
already carries every configuration necessary to interact
with AWS KMS. Whether you use environment variable with AWS
Credentials or let your AWS service assume role with permission
to interact with KMS, AWS SDK will handle the authentication.
Then we have the key
which should be the ARN of the AWS KMS key
or Alias of the key to be used. Finally we have context
. Context
is a non-secret information (e.g. it will be plain text on CloudTrail)
that allows you to bind your encryption with a specific signing
context. If the exact same context is not provided when trying
to decrypt
a specific cipher text, then the decryption will not
work. For instance, you may choose to use your service name as
the context of your encryption so that if other services accidentally
tries to decrypt your cipher texts, it won’t just work. The developer
would have to make a conscious decision to specify another service’s
context when trying to decrypt cross-service data.
For ease of use, we can base64_encode()
the cipher text and then
base64_decode
before decryption so that it’s easier to pass
the data around. If you’re interested in learning more about
base64_encode
, check out my post on Should I encrypt, hash or encode?.
Eloquent
Since the ServiceProvider is replacing the binding behind encrypter
on Laravel’s service provider, we’re free to use Eloquent’s
cast feature to encrypt/decrypt attributes automatically.
final class Credential extends TenantModel
{
protected $casts = [
'password' => 'encrypted',
'client_secret' => 'encrypted',
];
}
This way whenever we try to save something into the password
or
client_secret
attributes, Eloquent will use KmsEncrypter
to
encrypt the data being stored and when we’re accessing the attribute,
Eloquent Mutators will kick in and decrypt
it.
Tests
For my automation tests, I decided to use a NullEncrypter
implementation
so that I don’t need to integrate directly with AWS KMS to run
tests. Here’s how a NullEncrypter
could look like:
final class NullEncrypter implements Encrypter
{
public function encryptString(string $value): string
{
return $this->encrypt($value, false);
}
public function decryptString(string $value): string
{
return $this->decrypt($value, false);
}
public function encrypt($value, $serialize = true)
{
return $value;
}
public function decrypt($payload, $unserialize = true)
{
return $payload;
}
}
On test could, we could then replace the binding like this:
$this->app->bind(Encrypter::class, NullEncrypter::class);
$this->app->bind(StringEncrypter::class, NullEncrypter::class);
$this->app->bind('encrypter', NullEncrypter::class);
Watch out for your storage service
During the development of this implementation, I first wrote a produt
feature without encryption and handed the APIs over to the frontend
team so that they could get started. I then started implementing
AWS KMS encryption. I noticed that anytime my code would try to
decrypt
a cipher text, it would throw an exception saying
Error executing "Decrypt" on "https://kms.eu-west-1.amazonaws.com";
AWS HTTP error: Client error: `POST https://kms.eu-west-1.amazonaws.com` resulted in a `400 Bad Request` response:
{"__type":"InvalidCiphertextException"}
InvalidCiphertextException (client): - {"__type":"InvalidCiphertextException"}
The reason I was getting this error was not because of the key
nor the context. It was actually because I was interacting with
a legacy database with strict=false
and a password
field of
type varchar(191)
. What would then happen was that the cipher text
would go above 191 characters and MySQL would truncate the data
down to 191 characters. Losing part of a cipher text means that
we did not guarantee the integrity of the message and we can
no longer decrypt
it. Increasing the field size mitigated the
issue.
Why don’t we inform the key to the decrypt
API call to AWS?
When AWS encrypts a payload, it puts inside the cipher text an
identifier for which key was used for encryption. That way even
if we don’t know which key was used for encryption, AWS can still
decrypt
it as long as you have the complete cipher text and the
context. This probably facilitates AWS’s job when rotating keys.
When the key reaches 1 year of life, AWS will generate a brand new
one and start using it for any new encryption. It won’t get rid
of the old key, though. So if you make an API call asking for
decryption with a cipher text older than 1 year, AWS can still
find the identifier for the key used and decrypt
it.
Defining a Key with CloudFormation
The following template defines a MyEncryptionKey
resource and
a MyEncryptionKeyAlias
resource. It will also output the alias
alongside the Key ARN. The Key ARN can be used to attach to an IAM
Role that will need kms:Encrypt*
and kms:Decrypt*
. The Alias
can be used as an Environment Variable for your compute resource
so that it can be accessed by Laravel and injected into the
KmsEncrypter
class.
AWSTemplateFormatVersion: "2010-09-09"
Description: AWS KMS
Resources:
MyEncryptionKey:
Type: AWS::KMS::Key
Properties:
KeyUsage: ENCRYPT_DECRYPT
Description: Encryption key used for Encrypting/Decrypting sensitive data
EnableKeyRotation: true
KeyPolicy:
Version: '2012-10-17'
Id: EncryptionKey
Statement:
- Sid: Enable IAM Permissions
Effect: Allow
Principal:
AWS:
- !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: kms:*
Resource: '*'
MyEncryptionKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: alias/my-key-alias
TargetKeyId: !GetAtt GeneralEncryptionKey.Arn
Outputs:
MyEncryptionKeyAlias:
Description: Alias of Encryption key used for Encrypting/Decrypting sensitive data
Value: !Ref MyEncryptionKeyAlias
Export:
Name: MyEncryptionKeyAlias
MyEncryptionKeyArn:
Description: ARN of Encryption key used for Encrypting/Decrypting sensitive data
Value: !GetAtt MyEncryptionKeyArn.Arn
Export:
Name: MyEncryptionKeyArn
Conclusion
AWS KMS offering is great for the enterprise world. We will never
leak keys, they will be rotated automatically and it costs pennies
for the benefit that they bring. Laravel’s Encrypter
and StringEncrypter
interfaces makes it easy to swap the implementation and offer a
great DX to work with encryption be it directly or via Eloquent.
All in all it’s a great service, easy to use and designed to offer
safety.
It’s important to note that I’m not rolling my own encryption here. I’m swapping Laravel’s Encrypter class with one that uses AWS KMS. In other words, AWS KMS is responsible for encryption/decryption of the data and we should never roll our own encryption algorithms.
Follow me on Twitter to stay tuned with my latest work.
Cheers.
Recommend
-
13
Introduction I was recently doing some proof-of-concept work that required performing encryption using keys generated from AWS Key Management Serv...
-
11
请注意,本文编写于 723 天前,最后修改于 278 天前,其中某些信息可能已经过时。 使用 vlmcsd 搭建微软 KMS 激活服务器本文地址:blog.lucien.ink/archives/435
-
16
离线KMS激活工具 HEU KMS Activator v23.1 中文版...
-
16
AWS KMS 推出 Multi-region keys 這應該是 AWS 被許多大客戶敲碗許久的功能之一,AWS KMS 支援 global key:「
-
13
离线KMS激活工具 HEU KMS Activator v24.1 中文版...
-
15
KMS-VL-ALL批处理激活脚本是国外大神制作的一款微软全家桶Windows、Office的KMS激活工具,支持Windows 10专业版企业版激活,Windows 8 专业版企业版激活,支持Windows 7 旗舰版激活,支持 Office 2013激活Office 2016办公软件的激活,这款批处理版KMS激活脚本具...
-
14
Files Permalink Latest commit message Commit time
-
8
AWS KMS 與 AWS ACM 支援 post-quantum TLS ciphers AWS 宣佈 AWS KMS 與 AWS...
-
4
Share In my previous blog I wrote about application pipelines. These CodePipeline use a S3 bucket. What if you have a need for a customer managed key instead of the Amazon managed key? And you want to deploy the CloudFormation...
-
6
AWS KMS 降價 看到 AWS KMS 的公告:「
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK