如何使用 pyarrow 生成带 AAD prefix 的加密 parquet 文件?
结论
pyarrow 23.0.1 的公开 Python API 无法显式设置 AAD prefix。EncryptionConfiguration 不接受 aad_prefix,CryptoFactory.file_encryption_properties() 也没有对应参数,因此传入 aad_prefix="test_prefix" 会触发这个 TypeError。23.0.1 接口源码
可以按实际需求修改:
- 如果只需要生成加密 Parquet 文件,删除
aad_prefix,并修正 KMS 客户端的密钥封装逻辑。 - 如果文件必须使用
"test_prefix"作为 AAD prefix,就需要使用支持该配置的底层实现,例如 Arrow C++,或为 pyarrow 增加绑定。仅调整现有 Python 调用的参数无法完成。
为什么支持 Parquet 加密,却不能设置 AAD prefix?
Parquet 格式支持 AAD prefix,但各语言的 API 不一定开放了全部配置。
Parquet 的模块 AAD 包含可选的用户前缀和内部生成的后缀。前缀用于表达文件身份,例如表名、分区和版本;后缀用于区分文件及其内部模块。删除 aad_prefix 后,AAD 和列加密仍然生效,只是没有设置自定义前缀。Parquet AAD 规范
这个异常发生在创建 EncryptionConfiguration 时,程序还没有开始写入文件。由于接口没有这个参数,把 "test_prefix" 改成 b"test_prefix" 也无法解决。
如何修改脚本,先完成加密写入?
除了删除 aad_prefix,还需要修改原脚本中的这两个方法:
def wrap_key(self, key_bytes, master_key_identifier):
return key_bytes
def unwrap_key(self, wrapped_key, master_key_identifier):
return base64.b64decode(self.conf[master_key_identifier])
wrap_key() 接收待封装的密钥,unwrap_key() 则必须从封装结果中恢复出同一把密钥。原代码在封装时直接返回原始密钥,读取时却忽略 wrapped_key,返回固定的主密钥,两个操作并不匹配。pyarrow 要求这两个方法分别完成密钥封装和解封装。KmsClient 实现接口
可以用 cryptography 的 AES Key Wrap 实现一个本地演示客户端。先安装额外依赖:
python -m pip install cryptography
示例沿用原来的配置,只加密 salary 列,footer 保持明文,用于检查加密写入和解密读取流程。示例没有设置自定义 AAD prefix。密钥封装函数的说明见 cryptography 文档。
import base64
import pyarrow as pa
import pyarrow.parquet as pq
from cryptography.hazmat.primitives.keywrap import (
aes_key_wrap,
aes_key_unwrap,
)
from pyarrow.parquet.encryption import (
CryptoFactory,
EncryptionConfiguration,
KmsClient,
KmsConnectionConfig,
)
# 仅用于演示;实际使用时应通过安全的密钥管理方式提供主密钥。
MASTER_KEYS = {
"footer_key": b"0123456789abcdef0123456789abcdef",
"col_key": b"0123456789abcdef0123456789abcdef",
}
class SimpleKmsClient(KmsClient):
def __init__(self, master_keys):
super().__init__()
self.master_keys = master_keys
def wrap_key(self, key_bytes, master_key_identifier):
master_key = self.master_keys[master_key_identifier]
wrapped = aes_key_wrap(master_key, key_bytes)
return base64.b64encode(wrapped).decode("ascii")
def unwrap_key(self, wrapped_key, master_key_identifier):
master_key = self.master_keys[master_key_identifier]
wrapped = base64.b64decode(wrapped_key)
return aes_key_unwrap(master_key, wrapped)
def kms_client_factory(kms_connection_config):
return SimpleKmsClient(MASTER_KEYS)
data = pa.table({
"id": [1, 2, 3],
"name": ["Mark", "Mary", "Mike"],
"salary": [10001, 45345, 34345],
})
crypto_factory = CryptoFactory(kms_client_factory)
kms_conf = KmsConnectionConfig(kms_instance_id="local")
enc_config = EncryptionConfiguration(
footer_key="footer_key",
column_keys={"col_key": ["salary"]},
plaintext_footer=True,
double_wrapping=False,
internal_key_material=True,
)
file_encryption_props = crypto_factory.file_encryption_properties(
kms_conf,
enc_config,
)
path = "encrypt_columns_plaintext_footer.parquet"
pq.write_table(
data,
path,
encryption_properties=file_encryption_props,
)
# 读取并核对解密后的数据。
file_decryption_props = crypto_factory.file_decryption_properties(
kms_conf,
)
restored = pq.read_table(
path,
decryption_properties=file_decryption_props,
)
assert restored.equals(data)
print("File written and read back")
这里用 AES Key Wrap 保护密钥,再用 Base64 将封装后的二进制结果转换为文本。只对原始密钥做 Base64 编码不能代替密钥封装。
如果必须显式设置 "test_prefix",应该怎么做?
Arrow C++ 提供了 parquet::FileEncryptionProperties::Builder::aad_prefix(),可以在构建文件加密属性前调用:
// builder 是已配置密钥和列加密属性的
// parquet::FileEncryptionProperties::Builder。
builder.aad_prefix("test_prefix");
auto file_encryption_properties = builder.build();
这段代码需要配合 C++ Parquet writer 使用,不能直接放进 Python 脚本。构建器还提供 disable_aad_prefix_storage(),可以选择不把前缀存入文件。Arrow C++ 加密接口
如果仍希望从 Python 发起写入,可以编写 C++ 扩展来封装完整的写入过程,或者修改并重新编译 pyarrow,让 Python 绑定传递 AAD prefix。直接给现有 Python 对象添加同名属性,或把字符串放入普通 Parquet metadata,都不会让它参与加密认证。
使用时需要注意什么?
plaintext_footer=True 表示 footer 保持明文,并通过签名校验完整性;salary 列仍按配置加密。internal_key_material=True 表示相关密钥材料随文件保存,不代表文件中保存了明文主密钥。Parquet 加密规范、pyarrow 配置定义
如果用 AAD prefix 防止整文件替换,读取端还需要核对预期的文件身份。所有文件都使用固定的 "test_prefix",就无法靠它区分不同文件或版本。如果写入端选择不存储前缀,读取端必须通过支持该功能的接口提供相同前缀。AAD prefix 规范
备注:内容仅供参考。
