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. 使用 AEAD 密钥创建新的 ECIES。
  2. 将公钥以明文形式输出到输出文件。
  3. 将私钥输出到另一个输出文件。
  4. 在写入输出文件之前,使用托管在 Google Cloud(GCP)上的密钥加密生成的私钥(通常称为信封加密)。

我们要求以Tink KeysetJSON 格式存储/发布您的公钥。这允许其他 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 密钥集来从其 URL 接受 Google 公钥

https://news.google.com/swg/encryption/keys/prod/tink/public\_key

Google 公钥是一个Tink 密钥集,采用JSON 格式。请参阅此处,了解如何使用此密钥集的示例。

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

自动加密

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

步骤 3:集成授权器

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

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

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

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

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

请求管理

当请求到达您的授权器时

  1. 解析“crypt=”参数的授权 pingback URL。
  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 问题