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:创建公钥/私钥对

要加密文档的对称密钥,您需要有自己的公钥/私钥对。公钥加密是 混合加密 协议,具体而言,是带有 AES-GCM (128 位) 对称加密方法的 P-256 椭圆曲线 ECIES 非对称加密方法。

我们需要使用 Tink 通过 这种非对称密钥类型 来完成公钥处理。要创建私钥-公钥对,请使用以下任一方法

两者都支持密钥轮换。实施密钥轮换会限制因私钥泄露而产生的漏洞。

为了帮助您开始创建非对称密钥,我们创建了 此脚本。它会执行以下操作:

  1. 创建一个新的带有 AEAD 密钥的 ECIES。
  2. 将纯文本形式的公钥输出到输出文件。
  3. 将私钥输出到另一个输出文件。
  4. 在使用托管在 Google Cloud (GCP) 上的密钥写入输出文件之前,加密生成的私钥(通常称为 信封加密)。

我们需要以 Tink Keyset 的形式以 JSON 格式存储/发布您的公钥。这允许其他 AMP 提供的工具无缝工作。我们的脚本已经以这种格式输出公钥。

步骤 2:加密文章

决定您将手动加密付费内容,还是自动加密付费内容。

手动加密

我们需要使用 Tink 的 AES-GCM 128 对称方法来加密付费内容。用于加密付费内容的对称文档密钥对于每个文档都应是唯一的。将文档密钥添加到 JSON 对象,该 JSON 对象包含以 base64 编码的纯文本密钥,以及访问文档加密内容所需的 SKU。

以下 JSON 对象包含以 base64 编码的纯文本密钥和 SKU 的示例。

{
  AccessRequirements: ['thenewsynews.com:premium'],
  Key: 'aBcDef781-2-4/sjfdi',
}

使用在创建公钥/私钥对中生成的公钥加密上述 JSON 对象。

将加密结果作为值添加到密钥 "local"。将该键值对放在一个 JSON 对象中,该对象封装在 <script type="application/json" cryptokeys=""> 标记内。将该标记放在文档的 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 来从其 URL 接受 Google 公钥

https://news.google.com/swg/encryption/keys/prod/tink/public_key

Google 的公钥是以 Tink Keyset 的形式以 JSON 格式存储的。请参阅 此处,查看使用此密钥集的示例。

继续阅读:查看工作加密 AMP 文档的示例。

自动加密

使用我们的 脚本 加密文档。该脚本接受 HTML 文档,并加密 <section subscriptions-section="content" encrypted> 标记内的所有内容。使用传递给它的 URL 中的公钥,该脚本将加密由该脚本创建的文档密钥。使用此脚本可确保对所有内容进行正确编码和格式化以进行提供。请参阅 此处,了解有关使用此脚本的更多说明。

步骤 3:集成授权器

当用户具有正确的权利时,您需要更新授权器以解密文档密钥。amp-subscriptions 组件通过 “crypt=” URL 参数自动将加密的文档密钥发送到 "local" 授权器。它会执行以下操作

  1. "local" JSON 密钥字段解析文档密钥。
  2. 文档解密。

您必须在授权器中使用 Tink 解密文档密钥。要使用 Tink 解密,请使用在创建公钥/私钥对部分生成的私钥实例化一个 HybridDecrypt 客户端。在服务器启动时执行此操作以获得最佳性能。

您的 HybridDecrypt/授权器部署应大致与您的密钥轮换计划相匹配。这会创建 HybridDecrypt 客户端所有已生成密钥的可用性。

Tink 在 C++、Java、Go 和 Python 中提供了大量的 文档示例,以帮助您开始服务器端实施。

请求管理

当请求发送到您的授权器时

  1. 解析权利回传 URL 的 “crypt=” 参数。
  2. 使用 base64 解码 “crypt=” 参数值。存储在 URL 参数中的值是经过 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