AMP

使用用戶端加密保護您的訂閱內容

重要事項:本文件不適用於您目前選取的格式 故事

如果您是線上出版刊物,您可能仰賴訂閱者帶來收益。您可能會使用 CSS 混淆 (display: none) 在用戶端將優質內容阻擋在付費牆後方。

可惜的是,更精通技術的人可以繞過這個方法。

或者,您可能會向使用者顯示完全缺乏優質內容的文件!一旦您的後端驗證使用者,就會提供全新的頁面。雖然這種方法更安全,但會耗費時間、資源和使用者滿意度。

透過在用戶端實作優質訂閱者驗證和內容解密,即可解決這兩個問題。使用此解決方案,擁有優質存取權的使用者將能夠解密內容,而無需載入新頁面或等待後端回應!

設定總覽

若要實作用戶端解密,您將以下列方式結合對稱金鑰和公開金鑰加密

  1. 為每個文件建立隨機對稱金鑰,授予每個文件唯一金鑰。
  2. 使用文件的對稱金鑰加密優質內容。
    金鑰是對稱的,以便使用相同的金鑰來加密和解密內容。
  3. 使用公開金鑰加密文件金鑰,並使用 混合式加密協定來加密對稱金鑰。
  4. 使用 <amp-subscriptions> 和/或 <amp-subscriptions-google> 元件,將加密的文件金鑰儲存在 AMP 文件內,與加密的優質內容放在一起。

AMP 文件本身儲存加密金鑰。這樣可防止加密文件與解碼金鑰分離。

運作方式

  1. AMP 會從使用者登陸的文件上的加密內容中剖析金鑰。
  2. 在提供優質內容時,AMP 會將文件中的加密對稱金鑰傳送給授權者,作為使用者權利擷取的一部分。
  3. 授權者會判斷使用者是否具有正確的權限。如果有的話,授權者會使用其公私金鑰組中的私密金鑰解密文件的對稱金鑰。然後,授權者會將文件金鑰傳回 amp-subscriptions 元件邏輯
  4. AMP 會使用文件金鑰解密優質內容,並向使用者顯示!

實作步驟

按照下列步驟,將 AMP 加密處理與您的內部權利伺服器整合。

步驟 1:建立公私金鑰組

若要加密文件的對稱金鑰,您需要擁有自己的公私金鑰組。公開金鑰加密是一種 混合式加密 協定,特別是 P-256 橢圓曲線 ECIES 非對稱加密方法,搭配 AES-GCM (128 位元) 對稱加密方法。

我們要求使用 Tink此非對稱金鑰類型來完成公開金鑰處理。若要建立您的私密金鑰-公開金鑰組,請使用下列任一項

兩者都支援金鑰輪替。實作金鑰輪替可限制私密金鑰遭洩露的風險。

為了協助您開始建立非對稱金鑰,我們建立了這個指令碼。它的功能如下:

  1. 建立新的 ECIES 與 AEAD 金鑰。
  2. 以純文字形式將公開金鑰輸出到輸出檔案。
  3. 將私密金鑰輸出到另一個輸出檔案。
  4. 在使用 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" 授權者。它會執行

  1. "local" JSON 金鑰欄位剖析文件金鑰。
  2. 文件解密。

您必須在授權者中使用 Tink 來解密文件金鑰。若要使用 Tink 進行解密,請使用在「建立公私金鑰組」章節中產生的私密金鑰,例項化 HybridDecrypt 用戶端。在伺服器啟動時執行此操作,以獲得最佳效能。

您的 HybridDecrypt/授權者部署應大致符合您的金鑰輪替排程。這樣可讓所有產生的金鑰都可供 HybridDecrypt 用戶端使用。

Tink 具有廣泛的文件和 C++、Java、Go 和 Python 範例,可協助您開始伺服器端實作。

要求管理

當要求傳送到您的授權者時

  1. 剖析權利回呼網址以尋找 “crypt=” 參數。
  2. 使用 Base64 解碼 “crypt=” 參數值。網址參數中儲存的值是 Base64 編碼的加密 JSON 物件。
  3. 一旦加密金鑰以原始位元組形式存在,請使用 HybridDecrypt 的解密函式,使用您的私密金鑰解密金鑰。
  4. 如果解密成功,請將結果剖析為 JSON 物件。
  5. 驗證使用者是否具有存取權限,以存取「AccessRequirements」JSON 欄位中列出的其中一項權利。
  6. 從權利回應中的已解密 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