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) 上託管的金鑰將產生的私鑰寫入輸出檔案之前,先對其進行加密 (通常稱為 信封加密)。

我們要求以 Tink Keyset JSON 格式儲存/發布您的公開 Tink Keyset。這樣一來,其他 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" 授權者。它會執行

  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. 驗證使用者是否具有存取權限要求 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 問題