你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

为 Azure Cosmos DB 使用具有 Always Encrypted 功能的客户端加密

适用于: NoSQL

重要

我们的 1.0 版加密包引入了一项中断性变更。 如果你使用以前的版本创建了数据加密密钥和已启用加密的容器,则在将客户端代码迁移到 1.0 版的包后,需要重新创建数据库和容器。

Always Encrypted 功能旨在保护 Azure Cosmos DB 中存储的敏感数据,如信用卡号或国家/地区身份证号(例如美国社会安全号码)。 Always Encrypted 允许客户端对客户端应用程序内的敏感数据进行加密,并且永远不向数据库透漏加密密钥。

Always Encrypted 将客户端加密功能引入 Azure Cosmos DB。 在以下情况下,可能需要加密客户端数据:

  • 保护具有特定机密性特性的敏感数据:Always Encrypted 支持客户端在其应用程序中对敏感数据进行加密,并且永远不会向 Azure Cosmos DB 服务公开纯文本数据或加密密钥。
  • 为每个属性实现访问控制:由于加密是通过你拥有和管理的 Azure Key Vault 的密钥来控制的,因此可以应用访问策略来控制每个客户端可以访问哪些敏感属性。

概念

适用于 Azure Cosmos DB 的 Always Encrypted 引入了一些新的概念,这些新概念与客户端加密的配置有关。

加密密钥

数据加密密钥

使用 Always Encrypted 时,数据会通过应提前创建的数据加密密钥 (DEK) 进行加密。 这些 DEK 存储在 Azure Cosmos DB 服务中,并在数据库级别上定义,因此可以在多个容器之间共享 DEK。 使用 Azure Cosmos DB SDK 在客户端创建 DEK。

方法:

  • 为要加密的每个属性创建一个 DEK,或
  • 使用相同的 DEK 加密多个属性。

客户管理的密钥

DEK 在 Azure Cosmos DB 中存储前,将由客户管理的密钥 (CMK) 进行包装。 通过控制 DEK 的包装和解包,CMK 可以有效地控制对使用相应 DEK 加密的数据的访问。 CMK 存储设计为可扩展,默认实现要求它们存储在 Azure 密钥保管库中。

加密密钥

加密策略

索引策略类似,加密策略是描述应如何加密 JSON 属性的容器级规范。 创建容器时必须提供此策略,并且该策略是不可变的。 在当前版本中,无法更新加密策略。

对于要加密的每个属性,加密策略定义:

  • 属性的路径,格式为 /property。 目前仅支持顶层路径,不支持嵌套路径(如 /path/to/property)。
  • 加密和解密属性时使用的 DEK 的 ID。
  • 加密类型。 可以是随机类型或确定性类型。
  • 加密属性时使用的加密算法。 指定算法可以替代创建密钥时定义的算法(如果二者兼容)。

随机加密与确定性加密

Azure Cosmos DB 服务永远不会看到使用 Always Encrypted 加密的属性的纯文本。 但是,它仍支持对加密数据执行某些查询功能,具体取决于用于属性的加密类型。 Always Encrypted 支持以下两种类型的加密:

  • 确定性加密:对任何给定的纯文本值和加密配置始终生成相同的加密值。 通过确定性加密,查询可以对加密属性执行相等筛选。 但是,它可能会允许攻击者通过了解加密属性中的模式来推测有关加密值的信息。 当存在小部分可能的加密值时(如 True/False 或北部/南部/东部/西部区域),则尤其如此。

  • 随机加密:它使用一种以难以预测地方式加密数据的方法。 随机加密更加安全,但会妨碍查询对加密属性进行筛选。

请参阅生成初始化向量 (IV),详细了解 Always Encrypted 中的确定性加密和随机加密。

设置 Azure Key Vault

开始使用 Always Encrypted 的第一步是在 Azure Key Vault 中创建 CMK:

  1. 创建新的 Azure Key Vault 实例或浏览到现有实例。
  2. 在“密钥”部分中创建新密钥。
  3. 创建密钥后,浏览到它的当前版本,并复制它的完整密钥标识符:
    https://<my-key-vault>.vault.azure.net/keys/<key>/<version>。 如果在密钥标识符的末尾省略密钥版本,将使用最新版本的密钥。

接下来,需要配置 Azure Cosmos DB SDK 访问 Azure Key Vault 实例的方式。 此身份验证是通过 Microsoft Entra 标识完成的。 虽然可以使用任何类型的标识,但你很可能会使用 Microsoft Entra 应用程序的标识或托管标识作为客户端代码与 Azure Key Vault 实例之间的代理。 使用以下步骤将 Microsoft Entra 标识用作代理:

  1. 从 Azure Key Vault 实例中,浏览到“访问策略”部分,然后添加新策略:

    1. 在“密钥权限”中,选择“获取”、“列表”、“解包密钥”、“包装密钥”、“验证”和“签名” 。
    2. 在“选择主体”中,搜索 Microsoft Entra 标识。

保护 CMK 不被意外删除

若要确保在意外删除 CMK 后不会失去对加密数据的访问权限,建议在 Azure Key Vault 实例上设置两个属性:“软件删除”和“清除保护” 。

如果创建新的 Azure Key Vault 实例,请在创建过程中启用以下属性:

屏幕截图显示新 Azure Key Vault 实例的软删除和清除保护属性。

如果使用的是现有 Azure Key Vault 实例,则可以通过查看 Azure 门户中的“属性”部分来验证是否已启用这些属性。 如果未启用任一属性,请参阅以下文章中的“启用软删除”和“启用清除保护”部分:

初始化 SDK

注意

当前支持适用于 Azure Cosmos DB 的 Always Encrypted:

若要使用 Always Encrypted,必须将 KeyResolver 的实例附加到 Azure Cosmos DB SDK 实例。 此类在 Azure.Security.KeyVault.Keys.Cryptography 命名空间中定义,用于与托管 CMK 的密钥存储进行交互。

以下代码片段使用 DefaultAzureCredential 类来检索在访问 Azure 密钥保管库实例时要使用的 Microsoft Entra 标识。 可在此处找到创建不同类型的 TokenCredential 类的示例。

注意

你将需要额外的 Azure.Identity 包来访问这些 TokenCredential 类。

var tokenCredential = new DefaultAzureCredential();
var keyResolver = new KeyResolver(tokenCredential);
var client = new CosmosClient("<connection-string>")
    .WithEncryption(keyResolver, KeyEncryptionKeyResolverName.AzureKeyVault);

创建数据加密密钥

必须先在父数据库中创建数据加密密钥,然后才能在容器中加密数据。

通过调用 CreateClientEncryptionKeyAsync 方法并传递以下内容来创建新的数据加密密钥:

  • 一个字符串标识符,用于唯一标识数据库中的密钥。
  • 要与密钥一起使用的加密算法。 当前仅支持一个算法。
  • 存储在 Azure Key Vault 中的 CMK 的密钥标识符。 此参数在泛型 EncryptionKeyWrapMetadata 对象中传递,其中:
    • type 定义密钥解析器的类型(例如 Azure 密钥保管库)。
    • name 可以是所需的任意易记名称。
    • value 必须是密钥标识符。

    重要

    创建密钥后,浏览到其当前版本,并复制其完整密钥标识符:https://<my-key-vault>.vault.azure.net/keys/<key>/<version>。 如果在密钥标识符的末尾省略密钥版本,将使用最新版本的密钥。

    • algorithm 定义应使用哪种算法通过客户管理的密钥来包装密钥加密密钥。
var database = client.GetDatabase("my-database");
await database.CreateClientEncryptionKeyAsync(
    "my-key",
    DataEncryptionAlgorithm.AeadAes256CbcHmacSha256,
    new EncryptionKeyWrapMetadata(
        KeyEncryptionKeyResolverName.AzureKeyVault,
        "akvKey",
        "https://<my-key-vault>.vault.azure.net/keys/<key>/<version>",
        EncryptionAlgorithm.RsaOaep.ToString()));

创建具有加密策略的容器

创建容器时指定容器级加密策略。

var path1 = new ClientEncryptionIncludedPath
{
    Path = "/property1",
    ClientEncryptionKeyId = "my-key",
    EncryptionType = EncryptionType.Deterministic.ToString(),
    EncryptionAlgorithm = DataEncryptionAlgorithm.AeadAes256CbcHmacSha256
};
var path2 = new ClientEncryptionIncludedPath
{
    Path = "/property2",
    ClientEncryptionKeyId = "my-key",
    EncryptionType = EncryptionType.Randomized.ToString(),
    EncryptionAlgorithm = DataEncryptionAlgorithm.AeadAes256CbcHmacSha256
};
await database.DefineContainer("my-container", "/partition-key")
    .WithClientEncryptionPolicy()
    .WithIncludedPath(path1)
    .WithIncludedPath(path2)
    .Attach()
    .CreateAsync();

读取和写入加密数据

如何加密数据

每当将文档写入 Azure Cosmos DB 时,SDK 都会查找加密策略,以确定需要加密的属性以及如何加密。 加密的结果是一个 base 64 字符串。

复杂类型的加密:

  • 如果要加密的属性是 JSON 数组,数组的每个条目都会加密。

  • 如果要加密的属性是 JSON 对象,只会加密对象的叶值。 中间子属性名称以纯文本形式保留。

读取加密项

在发出点读取(按其 ID 和分区键提取单个项目)、查询或读取更改源时,无需显式操作即可对加密属性进行解密。 这是因为:

  • SDK 会查找加密策略,以确定需要解密哪些属性。
  • 加密的结果会嵌入值的原始 JSON 类型。

请注意,加密属性的解析及其后续解密仅基于请求返回的结果。 例如,如果 property1 已加密但投影到 property2 (SELECT property1 AS property2 FROM c) 中,则 SDK 接收时不会将其标识为加密属性。

针对加密属性的筛选查询

编写对加密属性进行筛选的查询时,必须使用特定的方法传递查询参数的值。 此方法采用以下参数:

  • 查询参数的名称。
  • 要在查询中使用的值。
  • 加密属性的路径(如加密策略中定义的)。

重要

加密属性只能在相等筛选器 (WHERE c.property = @Value) 中使用。 任何其他使用情况都将返回不可预知的和错误的查询结果。 这一约束将在 SDK 的下一版本中得到更好的执行。

var queryDefinition = container.CreateQueryDefinition(
    "SELECT * FROM c where c.property1 = @Property1");
await queryDefinition.AddParameterAsync(
    "@Property1",
    1234,
    "/property1");

当只能解密一部分属性时阅读文档

如果客户端无权访问用于加密属性的所有 CMK,则在数据读回后,只能对部分属性进行解密。 例如,如果 property1 用 key1 加密,而 property2 用 key2 加密,则仅具有 key1 访问权限的客户端应用程序仍可以读取数据,但不能读取 property2。 在这种情况下,必须通过 SQL 查询读取数据,并将客户端无法解密的属性投影出去:SELECT c.property1, c.property3 FROM c

CMK 轮换

如果怀疑当前 CMK 已泄露,你可能需要“轮换”你的 CMK(即使用新的 CMK 而不是当前的 CMK)。 定期轮换 CMK 也是一种常见的安全做法。 若要执行此轮换,只需提供应该用于包装特定 DEK 的新 CMK 的密钥标识符。 请注意,此操作不会影响数据的加密,但会影响 DEK 的保护。 在轮换完成之前,不应撤销对以前的 CMK 的访问权限。

await database.RewrapClientEncryptionKeyAsync(
    "my-key",
    new EncryptionKeyWrapMetadata(
        KeyEncryptionKeyResolverName.AzureKeyVault,
        "akvKey",
        "https://<my-key-vault>.vault.azure.net/keys/<new-key>/<version>",
        EncryptionAlgorithm.RsaOaep.ToString()));

DEK 轮换

执行数据加密密钥轮换的功能不是作为统包式功能提供的。 这是因为,更新 DEK 需要扫描使用此密钥的所有容器,并重新加密通过此密钥加密的所有属性。 此操作只能在客户端发生,因为 Azure Cosmos DB 服务不会存储或访问 DEK 的纯文本值。

在实践中,可以通过执行从受影响容器到新容器的数据迁移来完成 DEK 轮换。 新容器的创建方式与原始容器的创建方式完全相同。 可以在 GitHub 上找到一个独立的迁移工具来帮助实现这种数据迁移。

添加其他已加密的属性

出于上一部分中所述的相同原因,不支持将其他已加密的属性添加到现有加密策略。 此操作需要对容器进行完整扫描,以确保正确加密属性的所有实例,并且此操作只能在客户端发生。 如同 DEK 轮换一样,可以使用相应的加密策略将数据迁移新容器,以添加其他已加密的属性。

如果从架构的角度讲,你可以灵活地添加新的已加密属性,则还可以利用 Azure Cosmos DB 的架构不可知性。 如果将加密策略中定义的某个属性用作“属性包”,则可以在下面不受约束地添加其他属性。 例如,假设你的加密策略中定义了 property1,而你最初在文档中写入了 property1.property2。 如果在后一个阶段,你需要将 property3 添加为已加密的属性,则可以开始在文档中写入 property1.property3,而新属性也会自动加密。 此方法不需要进行任何数据迁移。

后续步骤