KMS – 云上数据加密和保护

本文讲解加密相关知识、信封加密原理、KMS密钥类型、KMS与实际服务集合使用。同时针对常见的加密需求,本文将介绍AWS Encrption SDK对文件加密场景,并给出AWSCLI、CLI命令示例、Python代码示例供参考。此外,本文还介绍了使用KMS保护Data Key并使用对称加密算法对字符串加密、非对称加密算法对字符串加密等场景,同时也给出了Python代码示例。

一、总体介绍

1、背景

在很多用户开始使用AWS的第一个阶段甚至大规模开始使用AWS的第二个阶段,都没有涉及到KMS服务和加密相关话题。KMS密钥无处不在,在EBS/RDS/S3等多种服务中都可能看到KMS相关选项和不同的加密概念,包括服务器端加密、客户端加密、公钥、私钥等,大量信息和术语需要理解,这导致KMS的使用门槛较高。然而在规范化、系统化的IT安全合规咨询和审计中,数据加密是必须采用的一项防护技术,对隐私保护和数据安全起着决定性作用。因此了解KMS的运行原理,并开始在托管服务中使用KMS是第一步,第二步可以在应用系统中使用KMS对数据进行加密,以充分满足合规要求。

2、Key Management Service(KMS)简介

KMS是一项密钥管理的托管服务。使用KMS服务的API,可以快速创建不同类型、不同算法的密钥,并将密钥置于KMS体系保护中。KMS可负责密钥授权、密钥禁用、到期轮换、加密和解密等任务。KMS与多种AWS服务深度集成,可为S3、EBS、RDS等提供数据加密服务。此外,用户也可以借助KMS生成自己的密钥,或者导入现有密钥。使用KMS服务,搭配可将执行加密的开销和代码工作量大幅降低。KMS以API方式调用,其背后基础设施的高可用能力由AWS保证。

使用KMS是AWS云上安全最佳实践之一。

注:KMS与Secret Manager有所区别。后者是AWS云上负责密码保管和轮换的服务,常用于避免在代码中Hardcode方式暴露密码字符串,用于诸如RDS数据库登陆密码等保护等场景。而KMS不管理字符串格式的密码,KMS生成和保护的密钥是证书密钥。因此,二者功能上没有交集也没有冲突。

3、CloudHSM简介

CloudHSM服务是AWS云端的专用的硬件安全模块 (HSM) 实例,通俗的说是云上加密机。使用CloudHSM可为密钥生成和保护提供独立的运行环境,在某些行业和场景的监管要求下是必须使用的独立组件。与在IDC自行托管加密机设备相比,CloudHSM具有更好的可用性、扩展能力,既满足了合规要求,又满足了云上不断变化的访问需求。

CloudHSM与KMS通过自定义密钥存储功能进行集成。在KMS界面上,用户可创建属于自己的独立的密钥存储设备,这将为用户分配一台CloudHSM加密机到用户指定的VPC,然后用户即可将自己创建的密钥(Custom Managed Key,缩写CMK)保存到自己专属的CloudHSM上。

本文重点介绍KMS,篇幅所限,对CloudHSM不展开介绍。

二、加密算法相关的基础概念介绍

1、对称和非对称加密的数据加密场景

加密的目的是保护数据。在加密场景下,有对称加密和非对称加密两个相对的概念。

对称加密指加密和解密使用相同的密钥。对称加密是最容易被理解的概念并且被广泛的使用。加密者使用密钥对数据加密,然后将密钥从另外的渠道告知数据使用者,数据使用者在使用相同的密钥对密文进行解密。对称加密使用场景广泛,运算速度快,通常用于体积较大的大量数据加密。对称加密主要算法是AES,在强度达到AES256后,在当前算力下人类可接受的时间内(数千万到数万年)无法完成破解,即便是当前的量子计算机也不能快速破解,因此被认为是暂时的量子安全。对称加密的主要挑战是密钥管理不够安全,加密和解密都使用同一个密钥,加密者可以查看相同密钥加密好的内容。

非对称加密是指加密和解密使用不同的密钥。非对称加密是通过复杂的数学算法,生成一个密钥对(key pair),分别叫做公钥(public key)和私钥(private key)。Public Key可以公开给所有人,任何希望发送数据的人,都可以用这个公开的公钥将数据加密钥,并持久化的保存下来。当需要查看数据的时候,只有持有私钥Private Key的用户才能执行解密。这样,就将加密和解密完全的隔离开,确保了使用场景上的安全。常见的非对称加密算法是RSA。非对称密钥由于需要更复杂的数学运算,因此需要的算力比对称加密要求更高。

AWS KMS的API支持由KMS使用对称或非对称算法来执行加密和解密。

2、非对称加密用于签名和验证

以上两个场景是用于数据加密和解密,此外还有一种场景是数据签名和校验。签名一般用于确认信息发送者、以及发送内容的真实身份。这种场景下,需要使用非对称加密算法。

例如信息发送者希望发送一段100字的公开信息,并且让接收者确认收到的信息正确、且发送人确实来自于发送者,那么可以这样操作:先对原始内容执行摘要算法,生成原始信息的摘要(可以叫做digest摘要或者叫做hash哈希)。过去广泛使用的MD5,如今使用较多的SHA256摘要算法,就是摘要算法。摘要算法是有损的且不可逆的,数学上无法将摘要结果反向恢复为字符串。获得摘要之后,发送者再使用自己手里的私钥(Private key)对摘要进行加密,然后将加密后的摘要的密文,以及原始消息本体一起对外发布。

接收者可以是从公开渠道,也可以是从私有渠道拿到消息原始文本和加密后的摘要,就可以进行校验。校验时候接收者首先自己对原始消息正文执行相同的摘要算法,获得摘要。然后将接收者收到的摘要密文使用手里的Public Key进行解密,这将获得发送者执行加密前的摘要。现在对比接收者接收者自己执行摘要算法计算获得的结果,和解密出来的发送者的摘要是否是完全相同。因为摘要算法的唯一性,任何对原始文本的篡改都会导致摘要结果不想等。因此这种方法,即可保证接收到的原始文件,是100%来自发送者的,中间没有经过篡改。这就是签名和校验的原理。

AWS KMS的API支持由KMS来执行签名和校验。

3、服务器端加密和客户端加密

服务器端Server-side encryption和客户端Client-side encryption加密二者是相互对照的概念。

服务器端加密:

  • 数据在服务器一侧由后台管理的加密算法进行加密,算法是服务预先定义好的,用户不能选
  • 写入后台前是明文(传输通道可以借助TLS保护安全)
  • 用户不需要编写加密代码,无编码工作量
  • 用户可以亲自掌握密钥,也可以不掌握委托后台服务自行管理密钥权限和轮换机制
  • 服务器端消耗加密算力,但大部分服务通过Nitro等技术实现加密Offload,加密不产生延迟,而客户端也不消耗算力
  • 通过服务后台的配置开关来确认加密是否成功(客户端肉眼不可见密文)

客户端加密:

  • 数据在应用侧由应用代码实现的加密算法进行加密,自己可选择任意加密算法
  • 写入后台之前已经是密文(传输通道依然需要TLS保护)
  • 用户需要自己编写加密代码,有编码工作量
  • 用户需要亲自掌握密钥,自行管理密钥权限和轮换机制(可不依赖KMS服务,也可以由KMS管理密钥)
  • 客户端消耗CPU算力,无法使用后台的Nitro技术加速,因此数据量大时候可能产生延迟
  • 客户端肉眼可见加密后的密文

从以上对比可以看出,很多AWS云服务如S3/EBS是默认使用服务器加密机制的。当然,用户可选在自己的应用代码中进行客户端加密,提供额外一层数据保护。

AWS KMS支持服务器端加密,也支持客户端加密。

4、AWS中国区域和海外区域的区别

在AWS中国区域,KMS服务使用的对称算法是国家密码局的商用密码算法:SM4。SM4的算法是公开的,最早于2012年提出,用于替代3DES、AES等加密算法并在2021年正式称为ISO国际标准之一的密码算法。SM4是属于对称密码的一种分组密码算法,分组长度和密钥长度均为128比特。在中国区的AWS云上使用KMS服务时,可额外增加参数指定算法是SM4,即可生成符合算法标准的密钥。

在AWS中国区域,KMS服务使用的非对称算法是国家密码局的商用密码算法:SM2。SM2的算法是公开的,是一种基于椭圆曲线密码(ECC)的公钥密码算法,用于替换RSA/ECC算法。SM2可用于数据加密、签名等场景。SM2的算法密钥为256位。在中国区的AWS云上使用KMS服务时,可额外增加参数指定算法是SM2,即可生成符合算法标准的密钥。

本文编写以AWS全球区域为例,因此不针对SM2/SM4进行单独讲解。

三、信封加密机制:KMS Key主密钥和Data Key数据密钥的使用场景

1、信封加密机制

信封加密机制是提升加密安全的一个重要机制。通常理解的加密是发送者使用一个单一密钥,对原始数据明文加密获得密文,然后密文和密钥分别移交给接收者。这样只有一个唯一的层级,是不够安全的使用方式。为此,引入信封加密机制。

信封加密机制是有两个密钥,一个是用于加密的数据的密钥,另一个是用于保护密钥的密钥。运作机制是先使用第一个密钥对明文数据做加密,加密之后获得数据的密文;接下来再使用第二个密钥,对第一个密钥的明文进行加密,获得了第一个密钥的密文。最后,将数据密文和第一个密钥的密文合并组合在一起,构成一个所谓的”信封“,这个信封即可发给接收者。第二个密钥将通过另外的方式,发送给接收者。

接收者收到信封后,首先将信封分解为第一个密钥的密文和数据的密文。然后接收者用第二个密钥对第一个密钥的密文进行解密,获得了第一个密钥的明文。现在就可以用第一个密钥的明文去解密原始数据了,最终获得原始数据。

信封加密机制的关键是将密文和被加密的密钥放在一起发送,这就类似于在一个信封内邮寄了一把钥匙,然后再把信封外层再锁上一把钥匙。信封加密机制可以提升安全性,例如可以频繁更换直接对数据加密的第一个密钥,这样攻击者无法捕捉到规律,提升破解难度。同时第二个密钥也可以很好的约定轮换,对当前已经发送的数据不收到影响可继续使用,能实现平滑的过度。

2、KMS Key主密钥和Data Key数据密钥

在信封加密中体系中,直接对数据进行加密的密钥是Data Key,对Data Key进行加密的密钥是则KMS主密钥。

在以前KMS的主密钥被称为Customer Master Key(缩写CMK),或者简称主密钥。CMK/主密钥这个名称的叫法今后将被KMS Key代替。KMS Key默认是对称密钥,用于加密、解密数据。KMS Key(主密钥)是在KMS服务中进行创建和管理,是无法被下载的,因此如果要使用主密钥做加密和解密,必须将数据送往KMS服务进行加密。

针对信封加密的场景,KMS服务提供了专门的API可以生成多种算法的Data Key数据密钥。此外,为符合信封加密机制的要求,KMS在创建Data Key后,还会用KMS主密钥对Data Key数据密钥本体进行加密生成Data Key的密文,用于保护Data Key不会以明文方式存储。

另外,为了隐私安全KMS服务只保存主密钥(一个账户内可以有多个主密钥),KMS不保存、不追踪Data Key数据密钥。例如一个用户首先创建了属于自己的一个主密钥,KMS中将能看到这个主密钥。然后用户调用KMS的API又创建了5个Data Key分别用于不同类型数据的加密。此时KMS服务不会保存、跟踪、显示这些Data Key,这些Data Key的保管/使用都是脱离KMS完全交给了用户的。KMS仅保护主密钥。

下边介绍KMS Key主密钥的类型。

3、KMS Key主密钥常见类型和使用场景

以上流程介绍了KMS Key主密钥和Data Key数据密钥的工作原理。下面针对主密钥,分别介绍三种KMS Key的类型,即AWS Owned Keys/AWS Managed Keys/Customer Managed Keys。

(1) AWS Owned Keys

AWS Owned Keys可翻译为AWS拥有的密钥,它是AWS构建后台服务时候的Key,常见于各种SaaS类(API调用)的场景,在服务账号中对于的数据落盘是加密的。AWS自身基础设置的有关合规认证是要求必须有数据加密的,这就是为什么用户即便不关心加密技术,AWS也要实现服务账号的加密能力。这个服务账号对用户是不可见的。

例如:通过API方式调用Bedrock图像识别,这时候在Bedrock服务账号后台的数据都是被AWS Owned Keys加密的。再例如,在S3创建存储桶时候,加密默认选项SSE-S3就是这种场景。如果您的IT合规审计要求自行管理密钥,那么则需要考虑另外的方式。

此场景没有任何费用。

(2) AWS Managed Keys

AWS Managed Keys可翻译为AWS托管密钥,它是用户在创建自己的服务时候,如果不选择额外创建自行管理的密钥,那么相应服务就会自动创建一个AWS Managed Keys。这个Key的创建/管理/轮换的过程是KMS服务自动完成的,您几乎无需管理。如果您创建对应服务时候,没有选了加密又没有指定Customer Managed Key,那么都会使用这种AWS Managed Keys来加密。

例如创建RDS时候,KMS那会自动创建出来一个专用于RDS加密的Key别名叫做aws/rds这样的名字。再例如,创建在S3创建存储桶时候,加密选项选择SSE-KMS,然后有一个默认已经存在的密钥aws/s3就是这样的场景。

此场景创建和管理密钥是没有费用的,仅在部分服务中使用这种AWS Managed Keys会有少量费用。例如在EC2的磁盘EBS/RDS中使用没有额外费用。再例如在S3的SSE-KMS场景中会有少量调用费用。这是因为每一次调用S3的API上传文件,如果选择了用AWS Managed Keys的aws/s3这个默认密钥来加密整个存储桶,那么每次写入文件都要触发密钥调用进行加密。此时,S3通过Bucket Key功能实现密钥缓存,可降低99%以上的S3 API调用产生的加密费用。这个Bucket Key的开关,就在创建S3存储桶时候的KMS配置位置可以打开。而且当你选了使用KMS后,这个选项默认打开。

如果从代码中以编程方式额外调用本密钥做加密,那么是会产生调用费用。在代码额外调用密钥进行加密,从使用场景上一般会选择用户自行创建的密钥用于加密,而不是选择系统默认的Key,因此这种使用场景就来到了第三个场景。

(3) Customer Managed Keys

Customer Managed Keys可翻译为用户托管的密钥,它是用户要求自行管理加密的场景下,自行创建/分配/管理/轮换的密钥。当用户IT合规审计有独立的加密要求的时候,就需要使用这种密钥场景。此时KMS服务和密钥管理都有专门的安全团队负责,其他AWS用户和服务只对KMS拥有最基本的权限。

Customer Managed Key使用中会有两种类型的费用,第一类是密钥托管的费用,密钥托管的费用是每个密钥每月1美金。创建后不足一个月的按时间比例计费(创建后存在的小时/720小时每月)。第二类是密钥调用的费用。AWS提供了每月20000次免费调用,超出之后每10000个请求0.03USD(以美东为例)。免费次数看起来不是很多,不过当使用信封加密机制时候,直接加密数据的是Data Key,因此可通过设计Data Key的机制,降低对KMS的

(4) 三种类型的KMS Key对比

注:这三种类型都是KMS Key,也就是以前说的Customer Master Key(CMK)主密钥。

名称谁来创建典型场景密钥托管收费密钥调用收费可用于应用层加密
AWS Owned Keys底层服务账号客户不可见S3存储桶创建时候什么也没做就是SSE-S3不收费不收费不可
AWS Managed Keys创建服务时打开加密的话KMS创建的默认密钥创建RDS时候打开了加密默认用的AWS/RDS密钥不收费调用收费(S3可大幅减免)可以但不推荐
Customer Managed Keys客户自己在KMS上创建和管理用户先在KMS创建好多个密钥再创建RDS时候选择匹配的密钥收费收费可以且建议使用Data Key加密

以上是三种密钥的区别。下边介绍两个KMS Key主密钥和Data Key数据密钥的实际使用场景。

四、KMS应用场景

1、KMS应用于托管服务(以EBS加密为例)的场景

在托管服务中,例如创建一个EC2上的EBS并打开加密,此时系统会调用KMS Key主密钥然后生成Data Key并使用Data Key来加密数据。其主要流程如下。

用户可感知的步骤:

  • KMS内有一个可用的KMS Key(主密钥)
  • 在创建EC2的EBS时候,选择使用某一个KMS Key主密钥
  • 磁盘创建完成,EBS可用

用户不可感知的背后的步骤:

  • EBS服务后台调用了KMS,以用户指定的KMS Key主密钥又创建了一个针对本EBS专用的Data Key,并且对Data Key也做了加密,加密后的Data Key的密文保留在本EBS磁盘的元数据中
  • EBS服务开始创建磁盘,使用创建好的Data key的明文对EBS磁盘完成加密,直到加密完成,加密完成后从内存中清除Data Key明文
  • EC2决定要使用并挂载这个加密的EBS磁盘了,此时EBS服务查找元数据里边保存的Data Key的密文,将Data Key密文送回到KMS解密,这就获得了本EBS使用的Data key明文,然后用这个明文对EBS磁盘解密,解密成功就挂载到上EC2正常使用

从以上过程可以看出,创建、挂载EBS时候都是需要把Data Key的密文先发给KMS解密获得Data Key明文,然后才能管用Data Key明文去对整个磁盘数据做操作的。因此,如果用户的KMS Key主密钥过期了,那么当前已经运行中的使用Data Key加密的服务是都不受影响的。但是,如果要启动新EBS或者原EC2停止后重启,那么都需要将Data Key密文发给KMS解密,此时KMS Key主密钥过期,那这些Data Key密文也就无法解密成Data Key明文了。缺少了Data Key明文,EBS磁盘也解不开了无法挂载到EC2使用。以上就是KMS应用于EBS服务的原理。

以上过程用户全程无感知,在用户代码将数据写入服务的后台时候自动完成加密,属于服务器端加密。

2、用户代码在应用层使用Data Key加密的场景

使用KMS生成的Data Key是完全脱离KMS服务并由用户进行管理的,其Data Key密文是可以被安全的保存下来,和应用程序保存在一起的。在用户进行应用层加密的时候,会按照如下流程。

加密流程:

  • 用户拥有自己的Customer Managed Keys作为KMS Key(主密钥)
  • 用户调用KMS的API,生成自己的Data Key,同时API返回明文Data Key(可选不返回明文),以及和加密的Data Key
  • 用户使用明文的Data Key对自己的用户数据进行加密,加密完成获得用户数据的密文,加密完成后,Data Key明文不再保留在内存中
  • 加密了的Data Key密文,和加密完成的用户数据密文一起保存下来

解密流程:

  • 首先将加密了的Data Key密文提交给KMS服务,调用KMS的解密API,获得Data Key明文
  • 使用Data Key明文对加密完成的用户数据进行解密
  • 解密完成后,从内存中移除Data Key明文

通过以上流程可以看出,KMS为实际执行加密的Data Key进行了额外保护,平时Data Key是以密文方式保存,增强了安全性。又因为KMS不保存Data key,因此可放心的将Data Key密文和数据保存在一起,有权限解密对KMS密文解密的用户即可最终解密出原始数据。

以上场景需要用户编码实现,在用户侧应用代码内先完成加密,然后在保存到S3存储桶/数据库中。由于编写加密代码不是后台完成是由用户自行完成,因此这种场景是客户端加密。即便这部分代码跑在EC2上/容器内,但其代码和逻辑依然是用户自行管理的,所以是客户端加密。为了更便于使用,针对客户端加密的场景,AWS提供了AWS Encryption SDK,并提供了CLI版本、C、Java、JS、Python等多种语言。这些SDK可以从Github上获取。

五、直接使用KMS Key进行加密和解密场景和API调用的例子(不使用Data Key)

1、为何不推荐跳过Data Key直接使用KMS加密

在AWS KMS服务提供的API上,包含了encryptdecrypt的API,可用于对Data Key加密也可以用于直接的数据加密,但是其API存在输入体积限制只能为不超过4K的文件加密。另外,调用KMS API存在调用费用,如果海量并发、高频写入调用KMS API,也会产生大量费用。根据前文讲述的KMS Key主密钥和Data Key的加密原理,使用信封加密方式用Data Key对数据加密是最佳实践。因此推荐使用KMS先去生成Data Key,并通过KMS加密Data Key将其保护起来,最后用Data key在对数据进行加密和解密。

本章节为了从技术角度讲解KMS保护Data Key的加密机制,因此这里提供了直接使用KMS Key加密/解密的例子。

2、使用AWSCLI在Linux/MacOS下对一个文件用KMS加密

构建如下的AWSCLI命令。其中路径的写法表示test01.txt在当前路径下。此外还需要AWSCLI配置为正确的region,或者通过增加--region xx-xxxx-1这样的参数指定region。

aws kms encrypt \
    --region us-west-2 \
    --key-id arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce  \
    --plaintext fileb://test01.txt \
    --output text \
    --query CiphertextBlob | base64 --decode > ExampleEncryptedFile

以上命令中,提交的是test01.txt这个文件,并且是以二进制方式提交的,KMS返回结果也是二进制文件。但是,因为AWSCLI在命令行控制台下,所以AWSCLI被设计为输出二进制数据时会自动进行Base64编码才输出/打印显示到CLI控制台。因此,为了获取加密后的原始数据,这里需要增加一个Base64的解码步骤,这样才能获得原始的密文数据。

由此,获得了加密后ExampleEncryptedFile这个文件。不管被加密的原文件是什么格式,加密后的文件一律是二进制文件。

3、使用AWSCLI在Linux/MacOS下将一个被加密的文件用KMS解密

构建如下的AWSCLI命令。其中路径的写法表示ExampleEncryptedFile在当前路径下。此外还需要AWSCLI配置为正确的region,或者通过增加--region xx-xxxx-1这样的参数指定region。

aws kms decrypt \
    --ciphertext-blob fileb://ExampleEncryptedFile \
    --key-id arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce  \
    --output text \
    --query Plaintext | base64 --decode > ExamplePlaintextFile

由此,获得了解密后的ExamplePlaintextFile这个文件。加密前的原始文件的格式如文本、图片、视频或者二进制,解密之后会保持加密前的原始格式。

六、使用AWS Encrption SDK(简称ESDK)在应用层进行加密

1、AWS Encrption SDK(简称ESDK)介绍

如前文介绍,使用信封加密机制也就是用Data Key加密,而不是用KMS Key主密钥加密是最佳实践。创建Data Key后,在加密或者解密时候需要调用KMS API对Data Key进行解密,然后用解密后的Data Key再去操作。这些步骤都需要用户自行编写代码完成多个步骤。

为了简化开发过程,可使用AWS Encrption SDK套件以更快的完成加密和解密操作。AWS Encrption SDK只需要指定KMS Key主密钥,即可自动的创建Data key。AWS Encrption SDK很适合于对大量不同用户的数据文件进行加密,默认情况下,AWS Encrption SDK为每一次加密用户数据都创建一个独立的Data Key,由此大大提升安全。AWS Encrption SDK创建的Key是Data Key,是不在KMS管理范围内的,因此KMS界面上将不会显示这些Data Key。

AWS Encrption SDK默认创建的Data Key是对称加密的密钥,加密算法是AES-GCM 256位。如果需要创建非对称密钥的话,需要额外增加参数指定密钥类型。例如加密场景可使用的是非对称密钥是RSA 4096算法,签名可使用ECDSA with P-384 and SHA-384算法。

2、AWS Encrption SDK的CLI版本使用

(1) 安装aws-encryption-sdk的CLI可执行版本

执行如下命令安装。注意,如果本机有多个Python版本安装到不同路径,请确认执行程序的Python3和安装软件包的Pip是同一个版本,同一个软件库。否则可能会出现其他目录下的Python3安装了依赖库,而执行程序的Python3报告找不到依赖库的情况。

/usr/local/bin/pip3 install awscli pypi aws-encryption-sdk-cli boto3 --upgrade

在通常情况下,aws-encryption-sdk的可执行版本CLI将会安装到对应版本的/Library/Frameworks/Python.framework/Versions/3.12/bin/aws-encryption-cli,这根据当前Python版本号不同,路径中的版本号可能轻微不同。

(2) 配置好AWSCLI的AKSK(Access Key/Secret Key)

当需要调用KMS服务时候,aws-encryption-cli会需要AWSCLI和AKSK。请参考相关文档。

(3) 创建KMS Key主密钥

略。这一步可以在AWS控制台图形界面上完成。请参考KMS文档。

(4) 对用户数据进行加密

/Library/Frameworks/Python.framework/Versions/3.12/bin/aws-encryption-cli \
    --encrypt \
    --input test01.txt \
    --wrapping-keys key=arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce \
    --metadata-output metadata-encrypt \
    --output ExampleEncryptedFile-ESDK

以上命令将当前目录下的test01.txt进行加密,使用的KMS主密钥是1234abcd-12ab-34cd-56ef-1234567890ab,命令会自动生成Data Key数据密钥,并且加密的数据密钥会保存在当前目录下的metadata文件中。最后加密完成的密文保存为ExampleEncryptedFile。注意在Java等SDK上,--encryption-context是必须输入参数,在CLI上是可选参数。以上就是加密全过程。

如果我们查看metadata文件,并将其JSON格式化便于阅读,那么可看到其格式如下。


{
  "header": {
    "algorithm": "AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384",
    "commitment_key": "WxycECHP7Cd5P26CHkSxvrtDzTs5ytIQjluFwHz4Yv4=",
    "content_type": 2,
    "encrypted_data_keys": [
      {
        "encrypted_data_key": "AQIBAHhQrG1c1HyMOTQ/+MMotI8T5rtu/5M0yDNuQTwDFyvcxwHs4x0zepm2B58QlvOn8LENAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMHxWkJHjKPzHZFBIhAgEQgDtjs63uyZi5V38G0l+V5cgFkVrrYe+bHjpeGoTCC62kzfMSYq+KlcwwxT7aWXf1EiD11JeWC1uykQjzvQ==",
        "key_provider": {
          "key_info": "YXJuOmF3czprbXM6dXMtd2VzdC0yOjEzMzEyOTA2NTExMDprZXkvOWZhYzRiZDMtZmJkNi00ZDM0LThmOTAtMTI3NDBjYmUwOWNl",
          "provider_id": "YXdzLWttcw=="
        }
      }
    ],
    "encryption_context": {
      "aws-crypto-public-key": "AnMnhy1D9fKixYPq1tY46LUUdeKiN0vHv8e69X3lv2O0zDDaQl7o4wj1z27vZmiw0A=="
    },
    "frame_length": 4096,
    "header_iv_length": null,
    "message_id": "vLSFumzX8+6Jei+l5KjBKVCEurb4gx2W/aRKIyFkNvE=",
    "type": null,
    "version": "2.0"
  },
  "input": "/Users/lxy/Downloads/test01.txt",
  "mode": "encrypt",
  "output": "/Users/lxy/Downloads/ExampleEncryptedFile2-ESDK"
}

这里就可以看到加密后的Data Key。

(5) 对用户数据进行解密

解密使用如下命令:

/Library/Frameworks/Python.framework/Versions/3.12/bin/aws-encryption-cli \
    --decrypt \
    --input ExampleEncryptedFile-ESDK \
    --wrapping-keys key=arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce \
    --metadata-output metadata-decrypt \
    --output ExamplePlainTextFile-ESDK

请注意在如上的命令中,与加密的命令不同的是,key的输入必须用ARN的完整格式输入,完整的ARN可以在KMS服务的控制台上查询到。另外解密也依然需要指定--metadata-output的信息,这个meta文件和加密时候的不是同一个,注意区分。

3、Python代码示例

上一节代码是以CLI为例,下面的代码以Python的Boto3的SDK为例,要求也是本机提前配置好AKSK。

(1) 安装aws-encryption-sdk软件包

注意,如果本机有多个Python版本安装到不同路径,请确认执行程序的Python3和安装软件包的Pip是同一个版本,同一个软件库。否则可能会出现其他目录下的Python3安装了依赖库,而执行程序的Python3报告找不到依赖库的情况。

/usr/local/bin/pip3 install awscli pypi aws-encryption-sdk-cli boto3 --upgrade

(2) 对一个字符串进行加密后再解密并进行对比验证

构建如下一段代码,命名为encrypt_and_decrypt.py。代码如下。

import aws_encryption_sdk
import base64

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"

# source string to be encrypted
source_plaintext = "This is my secret message!"

# Instantiate a client
client = aws_encryption_sdk.EncryptionSDKClient()

# Create a master key provider in strict mode
key_provider = aws_encryption_sdk.StrictAwsKmsMasterKeyProvider(key_ids=[kms_key_arn])

# Encrypt the plaintext - output is binary
cipher_text,encrypt_header, = client.encrypt(
    source = source_plaintext,
    key_provider = key_provider
)

print(cipher_text)

print("---")

print(encrypt_header)

print("---")

base64_string = base64.b64encode(cipher_text).decode('utf-8')
print(base64_string)

print("---")

decryption_text,decryption_header, = client.decrypt(
    source = cipher_text,
    key_provider = key_provider
)

print(decryption_text)

print("---")

print(decryption_header)

print("---")

在以上代码中,定义了一段要被加密的明文This is my secret message!,然后对这段先加密获得密文,再从密文解密获得明文。执行后结果如下:

b'\x02\x05x?\xf3\x16\x87\xacm\x11\xde\xfa\x9eG!\x80\xe7\x1e\xf3$\x8e\xa7\xe9a\xd9f\x97\x85\xd4\x1a\xbf9\xcd\xc1+\x00_\x00\x01\x00\x15aws-crypto-public-key\x00DAqBpZ5IburIEf82eBBU7mHQO3csjT6tB1o/eFDutPWcL2YBHC+fgLSMRk7d+DAf78Q==\x00\x01\x00\x07aws-kms\x00Karn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce\x00\xb8\x01\x02\x01\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01"\xd8\t\xa6)\x85y\x8f\xc24/v\x00\xc8\xc4\xb5\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\x08=\x13\xe0\xb2\x88\x0c4HV\xecu\x02\x01\x10\x80;\x1e\xed\x85\xc5\xab\x95>\x04o\x0e\x1f\x18\xd5C\xe6\xe1$\xf5\xc3\xa6M\x87\xf0\x99k\xea\x02\x0f\xb8\x97\x10\xa4\xad\n2\xa9\xab\xa0\xdbo\x8d\xd2\xf8T\xf7\xb0\xcc\xcd\x91\x90@c\x80>.g\x88\\,\x02\x00\x00\x10\x00\xbd\x07U\x0f\xf0\xf2baX5\xe8\xd6B\x1e\x08Mz\xe21\\\xb2\xee\xb1l\xbd\x8c|\xdci+\xa2n\n\xd9t\\\x88\xc1\x19\xcaj\x93eV\xa4\xb9\xd7\xae\xff\xff\xff\xff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1a\x97#\xcdo{My\x0f\xc0=\xf2\xc3q\xf9\xe1^bu\x11\xb7+\xa1\xc6\xdd\\\x0e\xf9\xe0\xd7\x14\xbd3i\xe0_\xe4\x80 \xd4\xcb\xd6w\x00g0e\x021\x00\xb7\xd7S\x0e\x88<\x88\xca\x0f\xb6\x01\x16\x1eiA\x944(7\x12\xce\xe4\xde\x04\xd4\x1f\x8f\xbf^\t\x93\x8d\\\xfd\x84\xfb\xc7\xa9(0\xe0\xffu\xec\x14\x98\x03j\x020\x0b3\xbc1\xe4\x02\x13/)\x80\xb1\xa6\xf4]\xf5\x7f\xf4\xff\xcdg\x14\x04\xc2H\x915D\x89\xab\xa99\xcb\xd2~Y\x9d\xab\xc6\xd9\xf4\x17\xad\xc0\xba\x91\xc2\x89c'
---
MessageHeader(version=<SerializationVersion.V2: 2>, algorithm=<AlgorithmSuite.AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384: (1400, <EncryptionSuite.AES_256_GCM_IV12_TAG16: (<class 'cryptography.hazmat.primitives.ciphers.algorithms.AES'>, <class 'cryptography.hazmat.primitives.ciphers.modes.GCM'>, 32, 12, 16)>, 2, <KDFSuite.HKDF_SHA512: (<class 'cryptography.hazmat.primitives.kdf.hkdf.HKDF'>, None, <class 'cryptography.hazmat.primitives.hashes.SHA512'>)>, <AuthenticationSuite.SHA256_ECDSA_P384: (<class 'cryptography.hazmat.primitives.asymmetric.ec.SECP384R1'>, <class 'cryptography.hazmat.primitives.hashes.SHA384'>, 103)>)>, message_id=b'?\xf3\x16\x87\xacm\x11\xde\xfa\x9eG!\x80\xe7\x1e\xf3$\x8e\xa7\xe9a\xd9f\x97\x85\xd4\x1a\xbf9\xcd\xc1+', encryption_context={'aws-crypto-public-key': 'AqBpZ5IburIEf82eBBU7mHQO3csjT6tB1o/eFDutPWcL2YBHC+fgLSMRk7d+DAf78Q=='}, encrypted_data_keys={EncryptedDataKey(key_provider=MasterKeyInfo(provider_id='aws-kms', key_info=b'arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce'), encrypted_data_key=b'\x01\x02\x01\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01"\xd8\t\xa6)\x85y\x8f\xc24/v\x00\xc8\xc4\xb5\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\x08=\x13\xe0\xb2\x88\x0c4HV\xecu\x02\x01\x10\x80;\x1e\xed\x85\xc5\xab\x95>\x04o\x0e\x1f\x18\xd5C\xe6\xe1$\xf5\xc3\xa6M\x87\xf0\x99k\xea\x02\x0f\xb8\x97\x10\xa4\xad\n2\xa9\xab\xa0\xdbo\x8d\xd2\xf8T\xf7\xb0\xcc\xcd\x91\x90@c\x80>.g\x88\\,')}, content_type=<ContentType.FRAMED_DATA: 2>, frame_length=4096, type=None, content_aad_length=None, header_iv_length=None, commitment_key=b'\xbd\x07U\x0f\xf0\xf2baX5\xe8\xd6B\x1e\x08Mz\xe21\\\xb2\xee\xb1l\xbd\x8c|\xdci+\xa2n')
---
AgV4P/MWh6xtEd76nkchgOce8ySOp+lh2WaXhdQavznNwSsAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREFxQnBaNUlidXJJRWY4MmVCQlU3bUhRTzNjc2pUNnRCMW8vZUZEdXRQV2NMMllCSEMrZmdMU01SazdkK0RBZjc4UT09AAEAB2F3cy1rbXMAS2Fybjphd3M6a21zOnVzLXdlc3QtMjoxMzMxMjkwNjUxMTA6a2V5LzlmYWM0YmQzLWZiZDYtNGQzNC04ZjkwLTEyNzQwY2JlMDljZQC4AQIBAHhQrG1c1HyMOTQ/+MMotI8T5rtu/5M0yDNuQTwDFyvcxwEi2AmmKYV5j8I0L3YAyMS1AAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMCD0T4LKIDDRIVux1AgEQgDse7YXFq5U+BG8OHxjVQ+bhJPXDpk2H8Jlr6gIPuJcQpK0KMqmroNtvjdL4VPewzM2RkEBjgD4uZ4hcLAIAABAAvQdVD/DyYmFYNejWQh4ITXriMVyy7rFsvYx83Gkrom4K2XRciMEZymqTZVakudeu/////wAAAAEAAAAAAAAAAAAAAAEAAAAalyPNb3tNeQ/APfLDcfnhXmJ1EbcrocbdXA754NcUvTNp4F/kgCDUy9Z3AGcwZQIxALfXUw6IPIjKD7YBFh5pQZQ0KDcSzuTeBNQfj79eCZONXP2E+8epKDDg/3XsFJgDagIwCzO8MeQCEy8pgLGm9F31f/T/zWcUBMJIkTVEiaupOcvSflmdq8bZ9BetwLqRwolj
---
b'This is my secret message!'
---
MessageHeader(version=<SerializationVersion.V2: 2>, algorithm=<AlgorithmSuite.AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384: (1400, <EncryptionSuite.AES_256_GCM_IV12_TAG16: (<class 'cryptography.hazmat.primitives.ciphers.algorithms.AES'>, <class 'cryptography.hazmat.primitives.ciphers.modes.GCM'>, 32, 12, 16)>, 2, <KDFSuite.HKDF_SHA512: (<class 'cryptography.hazmat.primitives.kdf.hkdf.HKDF'>, None, <class 'cryptography.hazmat.primitives.hashes.SHA512'>)>, <AuthenticationSuite.SHA256_ECDSA_P384: (<class 'cryptography.hazmat.primitives.asymmetric.ec.SECP384R1'>, <class 'cryptography.hazmat.primitives.hashes.SHA384'>, 103)>)>, message_id=b'?\xf3\x16\x87\xacm\x11\xde\xfa\x9eG!\x80\xe7\x1e\xf3$\x8e\xa7\xe9a\xd9f\x97\x85\xd4\x1a\xbf9\xcd\xc1+', encryption_context={'aws-crypto-public-key': 'AqBpZ5IburIEf82eBBU7mHQO3csjT6tB1o/eFDutPWcL2YBHC+fgLSMRk7d+DAf78Q=='}, encrypted_data_keys={EncryptedDataKey(key_provider=MasterKeyInfo(provider_id='aws-kms', key_info=b'arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce'), encrypted_data_key=b'\x01\x02\x01\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01"\xd8\t\xa6)\x85y\x8f\xc24/v\x00\xc8\xc4\xb5\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\x08=\x13\xe0\xb2\x88\x0c4HV\xecu\x02\x01\x10\x80;\x1e\xed\x85\xc5\xab\x95>\x04o\x0e\x1f\x18\xd5C\xe6\xe1$\xf5\xc3\xa6M\x87\xf0\x99k\xea\x02\x0f\xb8\x97\x10\xa4\xad\n2\xa9\xab\xa0\xdbo\x8d\xd2\xf8T\xf7\xb0\xcc\xcd\x91\x90@c\x80>.g\x88\\,')}, content_type=<ContentType.FRAMED_DATA: 2>, frame_length=4096, type=None, content_aad_length=None, header_iv_length=None, commitment_key=b'\xbd\x07U\x0f\xf0\xf2baX5\xe8\xd6B\x1e\x08Mz\xe21\\\xb2\xee\xb1l\xbd\x8c|\xdci+\xa2n')

在以上的返回结果中,第1段是AWS Encrption SDK加密后的密文的二进制格式,此密文按照信封加密原理同时包含加密后的正文以及加密后的Data Key,这一段是作为加密结果需要保留下来的。第2段是加密时候产生的Header,其中包含了加密的Data key密钥,这一段执行完毕后无需保留。第3段是将密文从二进制转换为base64方便保存。第4段是对第一段二进制格式的密文使用AWS Encrption SDK整体解密后,获得的原始字符串明文。第5段是解密时候产生的Header,这一段执行完毕后无需保留。

4、AWS Encrption SDK小结

从以上测试结果可以看出,AWS Encrption SDK是集成了KMS Key主密钥、Data Key数据密钥的整套SDK。AWS Encrption SDK为每一次加密都会独立生成给一个Data Key,并用KMS Key主密钥去加密Data Key。而不是多次加密共享同一个Data Key,由此带来很高的安全级别,适合多个用户数据各自独立加密的场景。此外,AWS Encrption SDK整合了对KMS生成Data key明文、加密Data Key明文、解密Data Key密文等多个步骤的自动操作,开发过程简单方便,推荐使用。

此外,AWS Encrption SDK也支持通过参数配置进行密钥共享,这意味着每次加密不会单独创建一个Data Key,用户可指定复用密钥的缓存时间、调用次数等配置。具体可参考本文末尾的AWS Encrption SDK文档。

七、使用KMS生成对称加密的Data Key的然后对某个字符串加密的例子

1、使用场景

上一个章节的测试中可看到,对一个字符串做加密时候AWS Encrption SDK也会每次都生成一个新的Data Key,然后封装为一个信封。这种场景对于加密单个文件比较友好。典型的场景是电商等业务中将用户Profile打包为一个完整的JSON文件,然后整体对这个JSON文件做加密。需要修改用户Profile时候再整体解密。这种场景下加密和解密的开销可以接受,并且AWS Encrption SDK为每个文件加密(对应一个用户Profile)都使用了独立的Data Key,安全级别非常高。

下面看另一个场景。如果场景是对数据库内某一个字段进行统一加密,例如希望对用户的地址信息在应用层进行对称加密,原始数据是字符串、且加密后依然是字符串,并将字符串保存在数据库中。在AWS Encrption SDK的工作机制中,每次对每个字符串都生成一个新的独立的Data Key,加密后密文是AWS Encrption SDK独有的数据格式,里边包含了加密的Meta信息且包含了加密的Data Key,密文长度变得很长,消耗数据库存储空间。

这时候对字符串的加密需求汇总为:

  • 高频使用接口,不希望每次加密解密都调用KMS去生成新的Data Key,同一列的所有数据用同一个Data Key加密是可接受的,并且能在应用层缓存
  • 存储开销最小化,因为是保存在数据库中的一列,因此加密后的字符串体积和规格不要与原先明文长度差别太大

在此场景下,可以考虑不使用AWS Encrption SDK,而是先调用KMS API生成Data Key并使用KMS保护Data Key,再编写应用代码调用Data Key对字符串加密。下边为代码例子。

2、从KMS创建Data Key(本例为对称密钥)

安装依存性包。注意,如果本机有多个Python版本安装到不同路径,请确认执行程序的Python3和安装软件包的Pip是同一个版本,同一个软件库。否则可能会出现其他目录下的Python3安装了依赖库,而执行程序的Python3报告找不到依赖库的情况。

/usr/local/bin/pip3 install boto3 pycryptodome --upgrade 

以Python代码为例。

import boto3

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"
region = 'us-west-2'

session = boto3.session.Session(region_name=region)
client = boto3.client('kms')

# Create data key, return both plaintext key and encrypted key.
response = client.generate_data_key(
    KeyId = kms_key_arn, 
    KeySpec = 'AES_256'
    )

print(response)

返回信息如下。

{
  'CiphertextBlob': b'\x01\x02\x03\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01\xb99\x9e\x9f\x1c*}\xafS\xea\xbc\x9a\xc2y\x14\x9c\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\x11^\xf0N\x97\xe9\xd0\x84g\x11\xae\x05\x02\x01\x10\x80;l\xb5\x93W\x8dT\x1d\xf5\x0ce\xd6\x1b\xe8)R\x9d\xd1\x89?\xf0\xce\x16\xd8\xbd7\x14\x9fr`\x16\xae\xa82\xc6\xaf\xa2\x08\xc1\xdf\xa6B\xb4\tH+\xbb\xf2J\x19\x00\x83\x97!\x8e\x02\x7f?\x80', 
  'Plaintext': b'\x83\xe7\xa3\x8d\x83\xd8\xd2\xfe\xba\\C][/3?\xac\xd1&F\xa9R,\xd4\x82\x86\xc8/\x11&l\x1c', 
  'KeyId': 'arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce', 
  'ResponseMetadata': {
    'RequestId': '2df07a4c-6d27-4177-92eb-3835ea399d94', 
    'HTTPStatusCode': 200, 
    'HTTPHeaders': {
      'x-amzn-requestid': '2df07a4c-6d27-4177-92eb-3835ea399d94', 
      'cache-control': 'no-cache, no-store, must-revalidate, private', 
      'expires': '0', 
      'pragma': 'no-cache', 
      'date': 'Sat, 31 Aug 2024 13:52:44 GMT', 
      'content-type': 'application/x-amz-json-1.1', 
      'content-length': '436', 
      'connection': 'keep-alive'
    }, 
      'RetryAttempts': 0
  }
}

以上信息可以看到,调用KMS的generate_data_key的API后,在Plaintext字段中返回了KMS生成的Data Key明文,且在CiphertextBlob返回了Data Key被加密后的密文。Data Key明文可以立刻用于加密数据,Data Key密文则和被加密的数据一起保存成为“信封”。

3、生成Data Key后立刻用Data Key明文对字符串进行加密

接上一个步骤,构建如下代码调用KMS API创建生成Data Key,并且立刻用创建好的Data Key对某一个字符串进行加密。

import boto3
import os

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"
region = 'us-west-2'

# Instantiate a client
session = boto3.session.Session(region_name=region)
client = boto3.client('kms')

# Create data key
def create_key():
    response = client.generate_data_key(
        KeyId = kms_key_arn, 
        KeySpec = 'AES_256'
        )
    plain_key = response['Plaintext']
    encrypted_key = response['CiphertextBlob']
    return plain_key, encrypted_key

def encrypt_aes_256(plaintext, key):
    # 确保密钥长度为32字节 (256位)
    if len(key) != 32:
        raise ValueError("Key must be 32 bytes long")
    # 生成一个随机的16字节初始化向量
    iv = os.urandom(16)
    # 创建一个加密器
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()
    # 对明文进行填充
    padder = padding.PKCS7(algorithms.AES.block_size).padder()
    padded_data = padder.update(plaintext.encode()) + padder.finalize()
    # 加密
    ciphertext = encryptor.update(padded_data) + encryptor.finalize()
    # 返回初始化向量和密文
    return iv + ciphertext

# 示例使用
plaintext = "this is my text"

# Generate data key from KMS, and get plain data key for immediate usage
aes_key_plain, aes_key_encrypted = create_key() 
print("data_key_plain (binary):", aes_key_plain)
print("---")
print("data_key_encrypted (binary):", aes_key_encrypted)
print("---")

# use data key to encrypt plain text
plaintext = "this is my text"
encrypted = encrypt_aes_256(plaintext, aes_key_plain)
print("Encrypted (hex):", encrypted.hex())

执行后返回如下:

data_key_plain (binary): b'\xeaWT\xe5\xa5\x1b6\x9d\x12\x13\x1a$=o\x9d\xcb\xf7\xcf\x8d\x82\xe8n%\x00M\xd38\xef$\xe3z.'
---
data_key_encrypted (binary): b'\x01\x02\x03\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01\x93\x9b\x15B\x84Bm9\x8c\xd8K\xe8\xae\x07\xed]\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\xa4\xcb\x9e\x1fE\xf0(\xb1\xf7(\xefC\x02\x01\x10\x80;|\xc8\x82\x11\x12&1h\xd1\xb2m[\xb4 \xb6,bP\xb9!\x9d\xb07\xd0\x95\xeeJ\x8d\x17\xea\xed}\xe4\x8cg\x82\xef\xbb\x9b\xa3\xdc}\xec/\x9c\xb9=\xc3\xd5\xc5URK\xf7\xc6\xb3\x0b\xc5\xec'
---
Encrypted (hex): 25520b616d4538c9c67f5fc87ad01ad1d60678d5bf1deea8c1dbab9f8c2a2f23

从以上信息中可以看到,代码返回了从KMS生成的Data Key明文(二进制),以及加密后的Data Key密文(二进制),最终并使用Data key对要加密的字符串做加密后的最终密文(十六进制字符串)。这个最终加密的密文,就可以放在数据库中的某一列保存。现在可以将Data Key明文抛弃掉不需要再保留在内存中。另外将Data Key的密文保存好。用于下一步解密。

4、对Data Key的密文进行解密

解密的根据信封加密的原理,先将Data Key的密文送往KMS服务,使用KMS Key(也就是主密钥)将Data Key解密。KMS讲Data key解密需要调用decrypt这个API。

这里使用上一步实验中的Data Key的密文,送回KMS解密。代码如下。

import boto3

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"
region = 'us-west-2'

# Data Key 密文(二进制)
data_key_encrypted = b'\x01\x02\x03\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01\x93\x9b\x15B\x84Bm9\x8c\xd8K\xe8\xae\x07\xed]\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\xa4\xcb\x9e\x1fE\xf0(\xb1\xf7(\xefC\x02\x01\x10\x80;|\xc8\x82\x11\x12&1h\xd1\xb2m[\xb4 \xb6,bP\xb9!\x9d\xb07\xd0\x95\xeeJ\x8d\x17\xea\xed}\xe4\x8cg\x82\xef\xbb\x9b\xa3\xdc}\xec/\x9c\xb9=\xc3\xd5\xc5URK\xf7\xc6\xb3\x0b\xc5\xec'

# Instantiate a client
session = boto3.session.Session(region_name=region)
client = boto3.client('kms')

# Decrypt data key
response = client.decrypt(
    CiphertextBlob = data_key_encrypted,
    KeyId = kms_key_arn
)

print(response)

在以上返回信息中,可以看到字段Plaintext就是Data Key的密文被KMS解密,由此获得了Data Key明文。

在实际使用场景中,作为信封加密机制要保护的核心,Data Key的明文应该是随用随销毁,不应该被print出来额外保存,本文给出的演示代码只是为了更快的学习理解才会print出明文的Data key。拿到Data Key的明文,应该立刻去对要解密的数据进行解密,然后从内存中清除掉Data Key的明文,才是安全最佳实践。

5、使用Data Key明文对字符串解密

接上一步,现在可以用Data Key明文去对数据解密了。这段代码同时完成Data Key解密和最终数据的解密。

import boto3

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"
region = 'us-west-2'

# Data Key 密文(二进制)
data_key_encrypted = b'\x01\x02\x03\x00xP\xacm\\\xd4|\x8c94?\xf8\xc3(\xb4\x8f\x13\xe6\xbbn\xff\x934\xc83nA<\x03\x17+\xdc\xc7\x01\x93\x9b\x15B\x84Bm9\x8c\xd8K\xe8\xae\x07\xed]\x00\x00\x00~0|\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0o0m\x02\x01\x000h\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\xa4\xcb\x9e\x1fE\xf0(\xb1\xf7(\xefC\x02\x01\x10\x80;|\xc8\x82\x11\x12&1h\xd1\xb2m[\xb4 \xb6,bP\xb9!\x9d\xb07\xd0\x95\xeeJ\x8d\x17\xea\xed}\xe4\x8cg\x82\xef\xbb\x9b\xa3\xdc}\xec/\x9c\xb9=\xc3\xd5\xc5URK\xf7\xc6\xb3\x0b\xc5\xec'
# 密文(HEX)
encrypt_text = '25520b616d4538c9c67f5fc87ad01ad1d60678d5bf1deea8c1dbab9f8c2a2f23'

# Instantiate a client
session = boto3.session.Session(region_name=region)
client = boto3.client('kms')

# Decrypt data key
def decrypt_data_key():
    response = client.decrypt(
        CiphertextBlob = data_key_encrypted,
        KeyId = kms_key_arn
    )
    return response['Plaintext']

def decrypt_aes_256(encrypted_data, key):
    # 确保密钥长度为32字节 (256位)
    if len(key) != 32:
        raise ValueError("Key must be 32 bytes long")
    # 从加密数据中提取IV(前16字节)和实际密文
    iv = encrypted_data[:16]
    ciphertext = encrypted_data[16:]
    # 创建一个解密器
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    decryptor = cipher.decryptor()
    # 解密
    padded_data = decryptor.update(ciphertext) + decryptor.finalize()
    # 移除填充
    unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
    data = unpadder.update(padded_data) + unpadder.finalize()
    # 返回解密后的明文
    return data.decode()

# Decrypt data key into plain (binary)
data_key_plain = decrypt_data_key()
# print("Decrypted Data Key:", data_key_plain)

# Decrypt your data into plain text
encrypted_data = bytes.fromhex(encrypt_text)
try:
    decrypted_text = decrypt_aes_256(encrypted_data, data_key_plain)
    print("Decrypted text:", decrypted_text)
except Exception as e:
    print("Decryption failed:", str(e))

返回结果如下:

Decrypted text: this is my text

可以看到解密成功。作为安全最佳实践,这里不再打印Data Key明文。程序执行完毕退出。为了进一步提升安全,建议在程序代码中,对完成解密后的Data Key这个变量明文的变量用0或者其他随机数填充。另外再辅助以其他第三方库进行内存回收(例如Python语言可使用secure-deletegc),确保内存中不会再留下之前使用过的Data Key明文。

由此就完成了一个对数据库某一列使用相同的Data Key进行加密和解密的操作。

八、使用KMS生成非对称加密的Data Key的然后对某个字符串加密的例子

1、非对称加密的使用场景

现在对上一个章节的方案寻找优化方案,可以发现对原始数据(也就是字符串)无论是加密还是解密都要用到Data Key明文,这也就是对称加密的特点。那么能否进一步提升安全,加密时候不需要Data Key明文,只有解密时候才需要Data Key明文?这个设想来自几个实际的需求场景:

  • 第一是出于提升安全性考虑,例如很多其他应用都希望有权限进行加密,即需要加密权限的人更多,但是读取数据解密是个小范围的权限。如果加密和解密是使用同一个密钥,那么就被迫让很多负责加密的应用获得了解密的密钥,这是安全的不足;
  • 第二是希望降低KMS的API调用次数,例如某应用写入量极大但是查询和修改量很小。那么使用对称加密在大量写入数据时候需要加密,对称加密的过程是需要完整的Data Key明文的,此时代码需要调用KMS API将Data Key密文解密为Data Key明文才可以继续加密,这样就产生大量KMS调用。此时的解决办法是进行密钥缓存,但如果管理不善也有安全风险。那么是否能让加密过程使用公开的密钥,完全不需要调用KMS的API即可完成加密?

以上场景中,推荐将非对称加密。在前文的示例代码中,可找到KMS生成密钥的API即generate_data_key,这个是生成对称密钥且返回明文的操作,为了使用非对称加密算法需要将其替换为generate_data_key_pair_without_plaintext。这个新的API将在调用KMS生成密钥时候,生成的Data Key将是一组非对称加密的密钥,KMS会返回Public Key的明文和Private Key的密文,Private Key的明文因为安全实践的原因将不会返回。接下来,任何应用代码都可使用Public Key的明文用于对数据加密。当且仅当需要对数据解密时候,先由KMS对Private Key的密文进行解密,获得原始的Private Key明文,再使用Private Key明文去解密原始数据。在从对称加密替换为非对称加密的过程中,可看到加密数据场景使用Public Key是更安全的场景。因为在非对称加密中Public Key是公开的,任何业务都可以使用Public Key的明文来加密,只有解密数据这个最核心的场景,才需要调用KMS去解密。

2、使用KMS生成非对称密钥Data Key

非对称密钥的算法也有多种,针对数据加密场景本例使用RSA4096作为数据加密的算法。如果是签名校验场景,可选ECC_NIST_P384等算法。

安装依存性包。注意,如果本机有多个Python版本安装到不同路径,请确认执行程序的Python3和安装软件包的Pip是同一个版本,同一个软件库。否则可能会出现其他目录下的Python3安装了依赖库,而执行程序的Python3报告找不到依赖库的情况。

/usr/local/bin/pip3 install boto3 pycryptodome cryptography --upgrade 

编写如下Python代码。

import boto3
from cryptography.hazmat.primitives import serialization

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"
region = 'us-west-2'

# Instantiate a client
session = boto3.session.Session(region_name=region)
client = boto3.client('kms')

# Create data key without plain private key output
response = client.generate_data_key_pair_without_plaintext(
    KeyId = kms_key_arn, 
    KeyPairSpec='RSA_4096'
)
# public key and private key 
binary_public_key = response['PublicKey']
binary_encrypted_private_key = response['PrivateKeyCiphertextBlob']

# Convert binary public key to PEM format
public_key = serialization.load_der_public_key(binary_public_key)
public_key_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')

print(public_key_pem)
print(binary_encrypted_private_key)

执行后打印出来Data Key的Public Key明文,以及Private Key的密文(二进制格式)。代码中增加了转换格式代码,Public Key打印为PEM格式方便其他应用加密使用。

3、使用Data Key的Public Key(明文)对字符串加密

接上一步,有了Data Key的Public Key明文,即可开始对数据加密。假设被加密的数据是一个字符串。

import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key

# 使用 RSA4096算法的公钥
pem_public_key = b"""
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx48Kfn3L33o2oQrmChQE
KXHqvZz6mxRJ4zP3wR805QnkKIJOoJM8hnh8OrzHIN81c26xmWPvaklS4jbJgLRd
eyn8nc6yxgmccq235uffMVJQiZhKef+/cDlUh6rq5PC1TVbmTcolXBGbZMnKsIWC
LPzCOPo1lKsH6pmw5N6gHPMa30Rq+FxmgqiyVJ8Uum6ID3EaHxLbKRj0TpJZYQNe
7J8+wG5HhpvLrJb4vdx9MvmWrvRYEykvAW0Dn5mlN+koPxae1lZ5XF8yGdxOoVz/
ZQS8TioafVwrb5iEUijxw8tCLcloDC8duRjyQo4sxD2AQk/o3ezwwJwWW17A/zh6
nsLe1d+mB2Ly6Plf5ztaLwvt7XHEAu8jNOfdMUbM2M4ruyA69xsf1CZyQM6AxPsk
CpFoXZLv1TSX9S1FhLj3mmU2XFTQRdMy1JjhFilQZiD4guMaLvEi8PaSmpdB6WGf
mOoQ3UxVKdo8iJ+f7V1hLPXGpSZfeYZnTC+hF89iNcq1D6Q2JK+eFk2ZyXr3fx3O
cDwdYtYDBD1YSYXBS/NKYJVMtmRiGWY2woMKIaByNXfv0Z3gJ7VrnFj/QJLzdT1p
a282IE01XtSF559ZrksUiLVEgpi18vb32DBP6CCJhMZSpfPTSwgo809ysMiLO1pf
jrRaKP9M1sH7z5+PqzBQwckCAwEAAQ==
-----END PUBLIC KEY-----
"""
plaintext = 'This is my text!'

# 加载公钥
public_key = load_pem_public_key(pem_public_key)
# 加密
ciphertext = public_key.encrypt(
    plaintext.encode('utf-8'),
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# 打印加密后的结果(以 base64 编码显示)

print("Encrypted:", base64.b64encode(ciphertext).decode('utf-8'))

执行结果如下。

Encrypted: AYa/6AHFMr+dKPcbKD0SegerJMsi5AzI606x9a5dvEvvJWzSQBoF/o5JWQy1ani1FiV3/k3prL7xDQ4h/l0pwaZ+hmpPG/EmaVxjAo2H0gLpI5cSH1l4QlMnRqC4r4ejfadtukvcdKmZI/JsHmbGpkUfoKY520qo6PuSb7sPSO/PIrcn43OMqJ8T7dRdl112T2MHNcg/HBI/GwPBov35dvgqgu3OFaXZzko8Y9irCYw2XYp7IOAAzjc+u88xWSEHssVxysnazsuuwMf63gVrrkFcQ9V4dVLUX3HQUE0UX08UOoJIz3Wbi4Vpv9lJq1qVMz+DY1Dz3m7RUGakpmUO8zNWdJBQEAy7Rt/PTOPsYdCfKAphr00kU0YIk16EeLl71rsU75HbLPhiK0KyoFt7pg7QQQnM17x77ZV0LulFOdtTZAKpom7BIaDBfPX10109nIDfLf3lN752aGfiuSiceRz0LOKsgdjM+pVU/AwOZbjEfHnGlvl0yHX5qSnB6uxTQ+Ed1BkMeCErdkQ0BbAAdvkcxx+cFTz7owmGewvBv1BXiucA1HCCCPnPw7f5vVMFxI/sWWWMVuj4Ecb3DJBbXBmfTwVfsuOmJa4A1062dY3GsPBvyaDqtw3xqEsEJpKFJuLdcY/4ilvm5wOn5oeZOKrUjwn3WMF53kW8QeOGl5U=

以上过程即使用Public Key的明文,完成了加密。

4、使用Data Key的Private key(密文)对字符串解密

接上一步,因为是非对称加密,所以有Public Key明文只能用于加密不能解密。在前边的步骤通过KMS创建密钥时候,我们也会在PrivateKeyCiphertextBlob字段中,收到Private Key的密文(二进制)。现在将Private key的密文送往KMS解密获得Private Key的明文,然后再使用这个Private Key的明文对最终数据进行解密。

构建如下Python代码。

import boto3
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key

# KMS Key/Master Key(CMK)
kms_key_arn = "arn:aws:kms:us-west-2:133129065110:key/9fac4bd3-fbd6-4d34-8f90-12740cbe09ce"
region = 'us-west-2'

# 原文加密后的密文
encrypted_base64 = 'okudlV9FlJCA9otJhHkmd7W36fElFj3SlAlV+iyEjhiO0X8FQlLOSFX1fhkpL0/K3q1LaXECBQ9nf4Ws3NOuXjL6Qy74qHTg59a39tMG+ZST8stAcvoBdaH7qSBkClhFlyfuCHv9o1anIZ9tfE5GN7TAX4d53m6g2TUDEl2IlFQxwAtLmumX5OVCvecB4/psK9WXz0p4VUXW4EPJda1AwIPtDEbs7W88yBo34mdjpMvNy8CqtM3/x0bU60abWs/Yd1KBW0ZKPYqTZFQAvghB7X615bC3QwmiY8BH3muShwdJ+oxyNx/utevnxdMfaw7cY6Q9mqdXmEGArH+I6lyAf7C9FnT1L6vrwA1y5oS8K4/sX8Fve4uD3+/fTa9A+2TT2bTYZGqzq8zMbSDf8x3rMEmAFqbpDcuI/2kgJbKci3Ql83+pZCjiE5rPPDCPhxmTLndsbt8ITbsb08Cr8CL3XHRKusAufPRA7Kn3KPZhfjPB3jlRFal0t6fOMa9NAIHCkNxw6fDCS5iKxGNKSQGImG+YJd0gnEkykjuqoPmvnQGJ5z5fVPUaNE0j2lHtSPyfPqUsp4wnpXRbSeGjzLD4tXiWRhvHJqMGQpa3dqdeZKMV1WWz2nHEyIMGtfFe4TVb+M4dDc71MjD3KYWf328Sog1mXPKMQE9LhFgjp8Cwqi4='

# Data Key 密文(二进制), 请替换为实际值
data_key_encrypted = b'\x01\x02\x03\x00 ....................... fN+\xf5\xd6\xdf\xcc88&|'

# Instantiate a client
session = boto3.session.Session(region_name=region)
client = boto3.client('kms')

# Decrypt data key
response = client.decrypt(
    CiphertextBlob = data_key_encrypted,
    KeyId = kms_key_arn
)

binary_private_key = response['Plaintext']
# print(binary_private_key)

def binary_to_pem(binary_key):
    # 从二进制数据加载私钥
    private_key = serialization.load_der_private_key(
        binary_key,
        password=None,
        backend=default_backend()
    )
    # 将私钥转换为PEM格式
    pem_key = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    return pem_key

# 转换为PEM格式
pem_private_key = binary_to_pem(binary_private_key)
# print(pem_private_key)

private_key = load_pem_private_key(pem_private_key, password=None)
ciphertext = base64.b64decode(encrypted_base64)

# 解密
plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# 打印解密后的结果
print("Decrypted:", plaintext.decode('utf-8'))

执行结果如下。

Decrypted: This is my text!

从以上结果可以看出,KMS首先负责对Private key的密文解密获得Private Key明文,然后使用Private Key明文对最终数据密文进行解密成功。

九、参考文档

更多密钥类型和使用场景

https://docs.aws.amazon.com/kms/latest/developerguide/key-types.html#symm-asymm-choose-key-spec

AWS Key Management Service 官方文档

https://docs.aws.amazon.com/zh_cn/kms/latest/developerguide/overview.html

Concepts in the AWS Encryption SDK

https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#envelope-encryption

aws-encryption-sdk-cli on Github

https://github.com/aws/aws-encryption-sdk-cli

Python sample from Github

[https://github.com/aws/aws-encryption-sdk-python/tree/master/examples/src])()

AWS Encryption SDK message format reference 加密后的输出格式

https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html

AWS SDK – Python Boto3 – KMS

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html