IBM Cloud Docs
使用 Parquet 模块化加密

使用 Parquet 模块化加密

IBM Analytics Engine Serverless 支持 Parquet 模块化加密,这是 Parquet 标准的新增功能,允许在写入 Parquet 文件时对敏感列进行加密,并在读取加密文件时对这些列进行解密。 请参阅 Parquet 模块化加密

除确保隐私外,Parquet 加密还将保护所存储数据的完整性。 检测文件内容的任何篡改并触发阅读器端异常。

主要功能包括:

  1. Parquet 加密和解密在 Spark 工作程序中执行。 因此,敏感数据和加密密钥对于存储器不可视。

  2. 标准 Parquet 功能(例如,编码、压缩、选取特定列和谓词下推)继续照常使用 Parquet 模块化加密格式处理文件。

  3. 您可以选择 Parquet 规范中定义的两种加密算法之一。 这两种算法都支持列加密,但是:

    • 缺省算法 AES-GCM 提供完全保护,防止篡改 Parquet 文件中的数据和元数据部分。
    • 备用算法 AES-GCM-CTR 支持 Parquet 文件的部分完整性保护。 仅保护元数据部分不被篡改,而非数据部分。 此算法的优点是与 AES-GCM 算法相比,其吞吐量开销较低。
  4. 您可以选择要加密的列。 不会对其他列进行加密,从而降低吞吐量开销。

  5. 可以使用不同的密钥对不同的列进行加密。

  6. 缺省情况下,加密主 Parquet 元数据模块(文件页脚)以隐藏文件模式和敏感列的列表。 但是,您可以选择不加密文件页脚,以便使旧阅读器 (例如其他尚不支持 Parquet 加密的 Spark 分发版) 能够读取加密文件中未加密的列。

  7. 可以采用以下两种方法之一来管理加密密钥:

    IBM® Key Protect for IBM Cloud® 可帮助您通过与 IBM Cloud Identity and Access Management (IAM) 角色对齐来管理加密密钥。

    : 仅需要管理主加密密钥 (MEK) (由应用程序管理或由 Key Protect管理)。 数据加密密钥 (DEK) 由 Spark/Parquet 自动进行处理。 有关 Parquet 加密中的密钥处理的详细信息,请参阅 加密密钥处理的内部

    对于每个敏感列,必须指定用于加密的主密钥。 此外,必须为每个加密文件 (数据帧) 的页脚指定主密钥-因为页脚会保留诸如模式和敏感列列表之类的元数据,这些元数据也可能是敏感的。 缺省情况下,页脚密钥将用于页脚加密。 但是,如果选择纯文本页脚方式,那么将不会加密页脚,并且密钥将仅用于页脚的完整性验证。

    可以通过标准 Spark Hadoop 配置传递加密参数,例如,通过在应用程序的 SparkContext的 Hadoop 配置中设置配置值:

    sc.hadoopConfiguration.set("<parameter name>" , "<parameter value>")
    

    或者,您可以通过写选项传递参数值:

    <data frame name>.write
    .option("<parameter name>" , "<parameter value>")
    .parquet("<write path>")
    

使用 Parquet 加密运行的必需参数

写入加密数据需要以下参数:

  • 要使用主加密密钥进行加密的列的列表:

    parameter name: "parquet.encryption.column.keys"
    parameter value: "<master key ID>:<column>,<column>;<master key ID>:<column>,.."
    
  • 页脚密钥:

    parameter name: "parquet.encryption.footer.key"
    parameter value: "<master key ID>"
    

    例如:

    dataFrame.write
    .option("parquet.encryption.footer.key" , "k1")
    .option("parquet.encryption.column.keys" , "k2:SSN,Address;k3:CreditCard")
    .parquet("<path to encrypted files>")
    

    重要信息: 如果既未设置 "parquet.encryption.column.keys" 参数,也未设置 "parquet.encryption.footer.key" 参数,那么将不会对文件进行加密。 如果仅设置其中一个参数,那么将抛出异常,因为这些参数对于加密文件是必需的。

  • 实现 EncryptionPropertiesFactory的类:

    parameter name: "parquet.crypto.factory.class"
    parameter value: "com.ibm.parquet.key.management.IBMKeyToolsFactory"
    

    重要信息: 如果未设置 "parquet.crypto.factory.class" ,那么加密工厂不会对文件进行加密。

可选参数

写入加密数据时可以使用以下可选参数:

  • 加密算法 AES-GCM-CTR

    缺省情况下, Parquet 加密使用 AES-GCM 算法,该算法提供完全保护,防止篡改 Parquet 文件中的数据和元数据。 但是,因为 Spark 2.3.0 在 Java 8 上运行,这不支持 CPU 硬件中的 AES 加速(仅在 Java 9 中添加此功能),在特定情况下,数据完整性验证的开销可能会影响工作负载吞吐量。

    要对此进行补偿,您可以关闭数据完整性验证支持,并使用备用算法 AES-GCM-CTR来写入加密文件,这将仅验证元数据部分的完整性,而不验证数据部分的完整性,并且与 AES-GCM 算法相比具有更低的吞吐量开销。

    parameter name: "parquet.encryption.algorithm"
    parameter value: "AES_GCM_CTR_V1"
    
  • 用于旧阅读器的纯文本页脚方式

    缺省情况下,加密主 Parquet 元数据模块(文件页脚)以隐藏文件模式和敏感列的列表。 但是,您可以决定不加密文件页脚,以便使其他 Spark 和 Parquet 阅读器 (尚不支持 Parquet 加密) 能够读取加密文件中未加密的列。 要关闭页脚加密,请设置以下参数:

    parameter name: "parquet.encryption.plaintext.footer"
    parameter value: "true"
    

    重要信息: 还必须在纯文本页脚方式下指定 "parquet.encryption.footer.key" 参数。 虽然页脚未加密,但使用密钥来签署页脚内容,这意味着新阅读器可以验证其完整性。 添加页脚签名不影响旧版阅读器。

用法示例

Python 和 Scala 的以下样本代码片段显示如何创建数据帧、写入已加密的 parquet 文件,以及读取加密的 parquet 文件。

  • Python: 编写加密数据

    from pyspark.sql import
    
    RowsquaresDF = spark.createDataFrame(
        sc.parallelize(range(1, 6))
        .map(lambda i: Row(int_column=i,  square_int_column=i ** 2)))
    
    sc._jsc.hadoopConfiguration().set("parquet.crypto.factory.class",
        "com.ibm.parquet.key.management.IBMKeyToolsFactory")
    sc._jsc.hadoopConfiguration().set("encryption.key.list",
        "key1: AAECAwQFBgcICQoLDA0ODw==, key2: AAECAAECAAECAAECAAECAA==")
    
    encryptedParquetPath = "squares.parquet.encrypted"squaresDF.write\
       .option("parquet.encryption.column.keys", "key1:square_int_column")\
       .option("parquet.encryption.footer.key", "key2")\
       .parquet(encryptedParquetPath)
    
  • Python: 读取加密数据

    sc._jsc.hadoopConfiguration().set("parquet.crypto.factory.class",
        "com.ibm.parquet.key.management.IBMKeyToolsFactory")
    sc._jsc.hadoopConfiguration().set("parquet.encryption.key.list",
         "key1: AAECAwQFBgcICQoLDA0ODw==, key2: AAECAAECAAECAAECAAECAA==")
    
    encryptedParquetPath = "squares.parquet.encrypted"
    parquetFile = spark.read.parquet(encryptedParquetPath)
    parquetFile.show()
    
  • Scala: 写入加密数据

    case class SquareItem(int_column: Int, square_int_column: Double)
    val dataRange = (1 to 6).toList
    val squares = sc.parallelize(dataRange.map(i => new SquareItem(i, scala.math.pow(i,2))))
    sc.hadoopConfiguration.set("parquet.crypto.factory.class","com.ibm.parquet.key.management.IBMKeyToolsFactory")
    sc.hadoopConfiguration.set("parquet.encryption.key.list","key1: AAECAwQFBgcICQoLDA0ODw==, key2: AAECAAECAAECAAECAAECAA==")
    val encryptedParquetPath = "squares.parquet.encrypted"
    squares.toDS().write
      .option("parquet.encryption.column.keys", "key1:square_int_column")
      .option("parquet.encryption.footer.key", "key2")
      .parquet(encryptedParquetPath)
    
  • Scala: 读取加密数据

    sc.hadoopConfiguration.set("parquet.crypto.factory.class","com.ibm.parquet.key.management.IBMKeyToolsFactory")
    sc.hadoopConfiguration.set("parquet.encryption.key.list","key1: AAECAwQFBgcICQoLDA0ODw==, key2: AAECAAECAAECAAECAAECAA==")
    val encryptedParquetPath = "squares.parquet.encrypted"
    val parquetFile = spark.read.parquet(encryptedParquetPath)
    parquetFile.show()
    

加密密钥处理的内部过程

写入 Parquet 文件时,将针对每个加密列和页脚生成随机数据加密密钥 (DEK)。 这些密钥用于加密 Parquet 文件中的数据和元数据模块。

然后,数据加密密钥将使用密钥加密密钥 (KEK) 进行加密,而且是在每个主密钥的 Spark/Parquet 中生成的。 密钥加密密钥使用主加密密钥 (MEK) 进行加密,在本地 (如果主密钥由应用程序管理) ,或者在 KeyProtect 服务中 (如果主密钥由 IBM® Key Protect for IBM Cloud®管理)。

加密的数据加密密钥和密钥加密密钥与主密钥标识一起存储在 Parquet 文件元数据中。 每个密钥加密密钥具有一个唯一标识(在本地作为安全随机 16 字节值生成),而且存储在文件元数据中。 密钥加密密钥通过访问令牌进行高速缓存,因此对于每个加密列或文件,如果它们使用相同的主加密密钥,那么无需与 KeyProtect 服务进行交互。

读取 Parquet 文件时,将从文件元数据中抽取主加密密钥 (MEK) 的标识、加密的密钥加密密钥 (KEK) 及其标识以及加密的数据加密密钥 (DEK)。

密钥加密密钥使用主加密密钥进行解密,如果主密钥由应用程序管理,那么在本地解密,如果主密钥由 IBM® Key Protect for IBM Cloud®管理,那么在 KeyProtect 服务中解密。 密钥加密密钥由访问令牌进行高速缓存,因此对于每个解密的列或文件,如果它们使用相同的密钥加密密钥,那么无需与 KeyProtect 服务进行交互。 然后,使用密钥加密密钥 (KEK) 在本地解密数据加密密钥 (DEK)。

密钥轮换 (可选功能部件)

包络加密实践具有 轮换 (更改) 主密钥的高级选项,以最大程度地降低因 MEK 受损而导致敏感数据泄露的风险。 无法更改 DEK ,因为这意味着完全再次加密 Parquet 文件。 新的 MEK (以及透明地重新生成的 KEK) 用于重新加密 DEK。

要在 Parquet 文件中启用密钥轮换,在编写 Parquet 文件时,必须在 Hadoop 配置中将 "parquet.encryption.key.material.store.internally" 参数设置为 "false"

通过此参数集, Parquet 将密钥 (用 MEK 打包) 存储在单独的小文件中,而不是存储在 Parquet 文件中。 执行密钥轮换时,仅替换小密钥材料文件。 无需修改 Parquet 文件 (这些文件通常不可更改)。 大多数 KMS 系统通过替换存储的主密钥的内容并保持 MEK 标识完整 (但更新密钥版本) 来支持密钥轮换。 用户或管理员使用特定于 KMS 的 API 在 KMS 实例中执行主密钥轮换后,可以通过调用以下命令来触发 Parquet 密钥轮换机制 (例如,由同一管理员):

public static void KeyToolkit.rotateMasterKeys(String folderPath, Configuration hadoopConfig)

执行密钥轮换时,将使用旧的 MEK 版本解包 folderPath 中所有密钥资料文件中的每个 KEK ,以启用 DEK 解密。 将为每个主密钥标识生成新的 KEK ,并使用新的 MEK 版本进行打包。 创建新的 KEK ,而不是复用旧的 KEK ,这不仅意味着安全性得到提高,还意味着后续数据读操作的性能得到提高。 这是因为由管理员运行的单个密钥轮换进程跨由不同进程创建的多个文件运行,这意味着针对同一 MEK 标识的许多不同 KEK。 密钥轮换过程将旧 KEK 替换为单个新的 KEK ,这允许阅读器对每个主密钥运行与 KMS 的单个解包交互。

有关如何使用密钥轮换的示例:

了解更多

请查看以下样本 Notebook ,以了解如何使用 Parquet 加密: