使用用戶端加密保護您的訂閱內容
重要事項:本文件不適用於您目前選取的格式 故事!
如果您是線上出版刊物,您可能仰賴訂閱者帶來收益。您可能會使用 CSS 混淆 (display: none
) 在用戶端將優質內容阻擋在付費牆後方。
可惜的是,更精通技術的人可以繞過這個方法。
或者,您可能會向使用者顯示完全缺乏優質內容的文件!一旦您的後端驗證使用者,就會提供全新的頁面。雖然這種方法更安全,但會耗費時間、資源和使用者滿意度。
透過在用戶端實作優質訂閱者驗證和內容解密,即可解決這兩個問題。使用此解決方案,擁有優質存取權的使用者將能夠解密內容,而無需載入新頁面或等待後端回應!
設定總覽
若要實作用戶端解密,您將以下列方式結合對稱金鑰和公開金鑰加密
- 為每個文件建立隨機對稱金鑰,授予每個文件唯一金鑰。
- 使用文件的對稱金鑰加密優質內容。
- 使用公開金鑰加密文件金鑰,並使用 混合式加密協定來加密對稱金鑰。
- 使用
<amp-subscriptions>
和/或<amp-subscriptions-google>
元件,將加密的文件金鑰儲存在 AMP 文件內,與加密的優質內容放在一起。
AMP 文件本身儲存加密金鑰。這樣可防止加密文件與解碼金鑰分離。
運作方式
- AMP 會從使用者登陸的文件上的加密內容中剖析金鑰。
- 在提供優質內容時,AMP 會將文件中的加密對稱金鑰傳送給授權者,作為使用者權利擷取的一部分。
- 授權者會判斷使用者是否具有正確的權限。如果有的話,授權者會使用其公私金鑰組中的私密金鑰解密文件的對稱金鑰。然後,授權者會將文件金鑰傳回 amp-subscriptions 元件邏輯。
- AMP 會使用文件金鑰解密優質內容,並向使用者顯示!
實作步驟
按照下列步驟,將 AMP 加密處理與您的內部權利伺服器整合。
步驟 1:建立公私金鑰組
若要加密文件的對稱金鑰,您需要擁有自己的公私金鑰組。公開金鑰加密是一種 混合式加密 協定,特別是 P-256 橢圓曲線 ECIES 非對稱加密方法,搭配 AES-GCM (128 位元) 對稱加密方法。
我們要求使用 Tink 和 此非對稱金鑰類型來完成公開金鑰處理。若要建立您的私密金鑰-公開金鑰組,請使用下列任一項
- Tink 的 KeysetManager 類別
- Tinkey (Tink 的金鑰公用程式工具)
兩者都支援金鑰輪替。實作金鑰輪替可限制私密金鑰遭洩露的風險。
為了協助您開始建立非對稱金鑰,我們建立了這個指令碼。它的功能如下:
- 建立新的 ECIES 與 AEAD 金鑰。
- 以純文字形式將公開金鑰輸出到輸出檔案。
- 將私密金鑰輸出到另一個輸出檔案。
- 在使用 Google Cloud (GCP) 上託管的金鑰將產生的私密金鑰加密後,再寫入輸出檔案 (通常稱為 Envelope Encryption)。
我們要求以 Tink Keyset JSON 格式儲存/發布您的公開金鑰。這樣可讓其他 AMP 提供的工具順暢運作。我們的指令碼已以這種格式輸出公開金鑰。
步驟 2:加密文章
決定您要手動加密優質內容,還是自動加密優質內容。
手動加密
我們要求使用 AES-GCM 128 對稱方法 (使用 Tink) 來加密優質內容。用於加密優質內容的對稱文件金鑰對於每個文件都應該是唯一的。將文件金鑰新增至 JSON 物件,該物件包含 Base64 編碼的純文字金鑰,以及存取文件加密內容所需的 SKU。
下方的 JSON 物件包含 Base64 編碼純文字金鑰和 SKU 的範例。
{
AccessRequirements: ['thenewsynews.com:premium'],
Key: 'aBcDef781-2-4/sjfdi',
}
使用在「建立公私金鑰組」中產生的公開金鑰,加密上述 JSON 物件。
將加密結果新增為金鑰 "local"
的值。將金鑰值組放在以 <script type="application/json" cryptokeys="">
標記包裝的 JSON 物件中。將標記放在文件的 head 中。
<head>
...
<script type="application/json" cryptokeys="">
{
"local": ['y0^r$t^ff'], // This is for your environment
"google.com": ['g00g|e$t^ff'], // This is for Google's environment
}
</script>
…
</head>
您需要使用本機環境和 Google 的公開金鑰加密文件金鑰。包含 Google 的公開金鑰可讓 Google AMP 快取服務您的文件。您必須例項化 Tink Keyset,以接受來自其網址的 Google 公開金鑰
https://news.google.com/swg/encryption/keys/prod/tink/public\_key
Google 的公開金鑰是 Tink Keyset,採用 JSON 格式。如需使用此金鑰組的範例,請參閱此處。
延伸閱讀:請參閱運作中的加密 AMP 文件範例。
自動加密
使用我們的 指令碼加密文件。此指令碼接受 HTML 文件,並加密 <section subscriptions-section="content" encrypted>
標記內的所有內容。使用傳遞給它的網址中找到的公開金鑰,此指令碼會加密指令碼建立的文件金鑰。使用此指令碼可確保所有內容都經過編碼和格式化,以便提供服務。如需關於使用此指令碼的進一步指示,請參閱此處。
步驟 3:整合授權者
當使用者擁有正確的權利時,您需要更新授權者以解密文件金鑰。amp-subscriptions 元件會透過 “crypt=” 網址參數,自動將加密的文件金鑰傳送給 "local"
授權者。它會執行
- 從
"local"
JSON 金鑰欄位剖析文件金鑰。 - 文件解密。
您必須在授權者中使用 Tink 來解密文件金鑰。若要使用 Tink 進行解密,請使用在「建立公私金鑰組」章節中產生的私密金鑰,例項化 HybridDecrypt 用戶端。在伺服器啟動時執行此操作,以獲得最佳效能。
您的 HybridDecrypt/授權者部署應大致符合您的金鑰輪替排程。這樣可讓所有產生的金鑰都可供 HybridDecrypt 用戶端使用。
Tink 具有廣泛的文件和 C++、Java、Go 和 Python 範例,可協助您開始伺服器端實作。
要求管理
當要求傳送到您的授權者時
- 剖析權利回呼網址以尋找 “crypt=” 參數。
- 使用 Base64 解碼 “crypt=” 參數值。網址參數中儲存的值是 Base64 編碼的加密 JSON 物件。
- 一旦加密金鑰以原始位元組形式存在,請使用 HybridDecrypt 的解密函式,使用您的私密金鑰解密金鑰。
- 如果解密成功,請將結果剖析為 JSON 物件。
- 驗證使用者是否具有存取權限,以存取「AccessRequirements」JSON 欄位中列出的其中一項權利。
- 從權利回應中的已解密 JSON 物件的「Key」欄位傳回文件金鑰。在權利回應中新增名為「decryptedDocumentKey」的新欄位,以加入已解密的文件金鑰。這會授予 AMP 架構的存取權。
下方的範例是虛擬程式碼片段,概述了上述說明步驟
string decryptDocumentKey(string encryptedKey, List < string > usersEntitlements,
HybridDecrypt hybridDecrypter) {
// 1. Base64 decode the input encrypted key.
bytes encryptedKeyBytes = base64.decode(encryptedKey);
// 2. Try to decrypt the encrypted key.
bytes decryptedKeyBytes;
try {
decryptedKeyBytes = hybridDecrypter.decrypt(
encryptedKeyBytes, null /* contextInfo */ );
} catch (error e) {
// Decryption error occurred. Handle it how you want.
LOG("Error occurred decrypting: ", e);
return "";
}
// 3. Parse the decrypted text into a JSON object.
string decryptedKey = new string(decryptedKeyBytes, UTF_8);
json::object decryptedParsedJson = JsonParser.parse(decryptedKey);
// 4. Check to see if the requesting user has the entitlements specified in
// the AccessRequirements section of the JSON object.
for (entitlement in usersEntitlements) {
if (decryptedParsedJson["AccessRequirements"]
.contains(entitlement)) {
// 5. Return the document key if the user has entitlements.
return decryptedParsedJson["Key"];
}
}
// User doesn't have correct requirements, return empty string.
return "";
}
JsonResponse getEntitlements(string requestUri) {
// Do normal handling of entitlements here…
List < string > usersEntitlements = getUsersEntitlementInfo();
// Check if request URI has "crypt" parameter.
String documentCrypt = requestUri.getQueryParameters().getFirst("crypt");
// If URI has "crypt" param, try to decrypt it.
string documentKey;
if (documentCrypt != null) {
documentKey = decryptDocumentKey(
documentCrypt,
usersEntitlements,
this.hybridDecrypter_);
}
// Construct JSON response.
JsonResponse response = JsonResponse {
signedEntitlements: getSignedEntitlements(),
isReadyToPay: getIsReadyToPay(),
};
if (!documentKey.empty()) {
response.decryptedDocumentKey = documentKey;
}
return response;
}
相關資源
查看 Tink Github 頁面上的文件和範例。
所有輔助指令碼都在 subscriptions-project/encryption Github 存放區中。
進一步支援
如有任何問題、意見或疑慮,請提交 Github Issue。
-
作者: @CrystalOnScript