使用用戶端加密保護您的訂閱內容
重要事項:本文件不適用於您目前選取的格式 電子郵件!
如果您是線上出版商,您可能仰賴訂閱者營收。您可能會使用 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) 上託管的金鑰加密產生的私密金鑰後,再寫入輸出檔案 (通常稱為 信封加密)。
我們要求以 Tink Keyset 的 JSON 格式 儲存/發布您的公開金鑰。這樣其他 AMP 提供的工具才能順暢運作。我們的指令碼已採用這種格式輸出公開金鑰。
步驟 2:加密文章
決定您要手動加密付費內容,還是自動加密付費內容。
手動加密
我們要求使用 Tink 的 AES-GCM 128 對稱方法來加密付費內容。用於加密付費內容的對稱文件金鑰對於每份文件都應該是獨一無二的。將文件金鑰新增至 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