AMP

使用客户端加密保护您的订阅内容

如果您是一家在线出版物,您可能依赖于订阅者来获得收入。您可能会在客户端使用 CSS 混淆 (display: none) 在付费墙后面阻止优质内容。

Premium content is hidden until users are authenticated.

不幸的是,技术更精通的人可以绕过这一点。

相反,您可能正在向用户展示一个完全缺乏优质内容的文档!一旦您的后端验证了用户,就会提供一个全新的页面。虽然更安全,但这种方法会花费时间、资源和用户满意度。

通过在客户端实现高级订阅者验证和内容解密来解决这两个问题。有了这个解决方案,具有高级访问权限的用户将能够解密内容,而无需加载新页面或等待后端响应!

设置概述

要实现客户端解密,您将按以下方式组合对称密钥和公钥加密

  1. 为每个文档创建一个随机对称密钥,授予每个文档一个唯一密钥。
    Unique keys for each unique document.
  2. 使用其文档的对称密钥加密优质内容。
    Use the document key to encrypt premium content.
    该密钥是对称的,允许相同的密钥加密和解密内容。
    The same key that encrypts the document also decrypts it.
  3. 使用公钥加密文档密钥,使用 混合加密协议来加密对称密钥。
    A hybrid encryption protocol encrypts the symmetric key with a public key.
  4. 使用 <amp-subscriptions> 和/或 <amp-subscriptions-google> 组件,将加密的文档密钥存储在 AMP 文档中,与加密的优质内容一起存储。
    Both keys are stored inside of the AMP document.

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 对象中,该对象包含以 base64 编码的纯文本形式的密钥,以及访问文档加密内容所需的 SKU。

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

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

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

将加密结果添加为密钥 "local" 的值。将键值对放在包装在 <script type="application/json" cryptokeys=""> 标记内的 JSON 对象中。将标记放置在文档的头部。

<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