先講一個容易做錯的地方。
FPE 需要一把金鑰。最直覺的做法是產生一把金鑰、存在設定檔裡、呼叫 API 時帶上。這是錯的,因為:
正確的架構是三層:
KEK(Key Encryption Key) 存在 Cloud KMS,永遠不離開 KMS
↓ 加密
DEK(Data Encryption Key) 加密後的形式存在你的設定裡
↓ 實際用來做 FPE
Token
你的應用程式手上只有「被 KEK 加密過的 DEK」(wrapped key)。SDP 呼叫時把 wrapped key 交給它,SDP 去 KMS 解開,用完就丟。
你的應用程式從頭到尾沒有摸過明文金鑰。
# 1. 建立 KMS keyring 與 key
gcloud kms keyrings create pii-vault-ring \
--location asia-east1
gcloud kms keys create pii-fpe-kek \
--location asia-east1 \
--keyring pii-vault-ring \
--purpose encryption \
--rotation-period 90d \
--next-rotation-time "$(date -u -d '+90 days' +%Y-%m-%dT%H:%M:%SZ)"
# 2. 產生 DEK(AES-256)
openssl rand -out dek.bin 32
# 3. 用 KEK 包裝 DEK
gcloud kms encrypt \
--location asia-east1 \
--keyring pii-vault-ring \
--key pii-fpe-kek \
--plaintext-file dek.bin \
--ciphertext-file dek-wrapped.bin
# 4. 立刻銷毀明文 DEK
shred -u dek.bin
第 4 步不能省。明文 DEK 存在磁碟上的每一秒都是風險。
--rotation-period 90d 設定 KEK 自動輪替。要注意 KEK 輪替不影響已經包裝好的 DEK(KMS 會保留舊版本用於解密),但如果你要輪替 DEK,那所有既有 Token 都需要重新產生——這是一個需要規劃的維運事件,D12 會談。
import base64
from google.cloud import dlp_v2
dlp = dlp_v2.DlpServiceClient()
PARENT = "projects/YOUR_PROJECT/locations/asia-east1"
with open("dek-wrapped.bin", "rb") as f:
WRAPPED_DEK = base64.b64encode(f.read()).decode()
CRYPTO_KEY = {
"kms_wrapped": {
"wrapped_key": WRAPPED_DEK,
"crypto_key_name": (
"projects/YOUR_PROJECT/locations/asia-east1/"
"keyRings/pii-vault-ring/cryptoKeys/pii-fpe-kek"
),
}
}
deidentify_config = {
"info_type_transformations": {
"transformations": [
# 身分證字號:FPE,數字字元集
{
"info_types": [{"name": "TW_ID"}],
"primitive_transformation": {
"crypto_replace_ffx_fpe_config": {
"crypto_key": CRYPTO_KEY,
"common_alphabet": "ALPHA_NUMERIC",
"surrogate_info_type": {"name": "TOKEN_TWID"},
}
},
},
# 電話:FPE,純數字
{
"info_types": [{"name": "PHONE_NUMBER"}],
"primitive_transformation": {
"crypto_replace_ffx_fpe_config": {
"crypto_key": CRYPTO_KEY,
"common_alphabet": "NUMERIC",
"surrogate_info_type": {"name": "TOKEN_PHONE"},
}
},
},
# 健康資訊:遮罩,不可還原
{
"info_types": [{"name": "HEALTH_INFO"}],
"primitive_transformation": {
"replace_config": {
"new_value": {"string_value": "【健康資訊已遮蔽】"}
}
},
},
# 出生日期:概化到年
{
"info_types": [{"name": "DATE_OF_BIRTH"}],
"primitive_transformation": {
"date_shift_config": {
"upper_bound_days": 180,
"lower_bound_days": -180,
"crypto_key": CRYPTO_KEY,
}
},
},
]
}
}
def deidentify(text: str) -> str:
resp = dlp.deidentify_content(
request={
"parent": PARENT,
"deidentify_config": deidentify_config,
"inspect_config": INSPECT_CONFIG, # 見 Day 7
"item": {"value": text},
}
)
return resp.item.value
注意每個 FPE 設定裡的 surrogate_info_type。它的作用是在輸出裡標記這是一個 Token:
原文: 身分證字號 A123456789
輸出: 身分證字號 TOKEN_TWID(B847263915)
還原時,SDP 靠這個標記知道哪些片段要還原、用哪個設定還原。
這帶來一個實務問題:這個標記會出現在送給 LLM 的文字裡,看起來很醜,也可能干擾模型。
兩種處理方式:
方式一:送出前剝除標記、還原前補回去。 需要自己維護一份「哪個位置是哪種 Token」的側錄資訊。
import re
SURROGATE_RE = re.compile(r"(TOKEN_\w+)\(([^)]+)\)")
def strip_surrogates(text):
mapping = []
def _sub(m):
mapping.append({"type": m.group(1), "value": m.group(2)})
return m.group(2)
return SURROGATE_RE.sub(_sub, text), mapping
方式二:接受標記存在,並在 prompt 裡說明。 例如 system prompt 加一句「文中 TOKEN_XXX(...) 格式為去識別化標記,請原樣保留、不要改寫」。
我偏好方式一,因為方式二依賴模型聽話——而模型不一定聽話,尤其在文件內容裡有 injection 的時候(D22)。不要把資料完整性寄託在模型的服從性上。
reidentify_config = {
"info_type_transformations": {
"transformations": [
{
"info_types": [{"name": "TOKEN_TWID"}],
"primitive_transformation": {
"crypto_replace_ffx_fpe_config": {
"crypto_key": CRYPTO_KEY,
"common_alphabet": "ALPHA_NUMERIC",
"surrogate_info_type": {"name": "TOKEN_TWID"},
}
},
},
]
}
}
def reidentify(text: str) -> str:
resp = dlp.reidentify_content(
request={
"parent": PARENT,
"reidentify_config": reidentify_config,
"inspect_config": {
"custom_info_types": [
{
"info_type": {"name": "TOKEN_TWID"},
"surrogate_type": {},
}
]
},
"item": {"value": text},
}
)
return resp.item.value
這段程式碼絕對不能放在一般的應用服務裡。 它需要 KMS 的解密權限,等於握有還原能力。它應該:
cloudkms.cryptoKeyVersions.useToDecrypt 的身分這就是 D2 說的「邊界二:應用/Vault」。
值得注意的是,用 FPE 做的 Token 化不需要 Vault——還原是靠金鑰算回去的,不是靠查表。
這是一個很大的優勢:沒有 Mapping Table,就沒有 Mapping Table 被偷的風險。
但它也有限制:
所以實際架構通常是混合:規整欄位走無狀態 FPE,自由文字走 Vault 查表。這樣 Vault 裡只存人名和地址,體積小很多,風險也小很多。
明天談 Vault:那些不能靠算的、只能靠存的部分。
我是 Fngi,專注在 AI 資安、LLM 紅隊與 AI 治理框架落地。
IG:@aid3fend
有想討論的架構細節或不同意見,留言或私訊都歡迎。