使用客户端加密保护您的订阅内容
重要提示:此文档不适用于您当前选择的格式故事!
如果您是线上出版物,您可能依赖订阅者获取收入。您可能会使用 CSS 混淆 (display: none
) 在客户端将高级内容隐藏在付费墙后面。
不幸的是,更多精通技术的人可以绕过它。
相反,您可能正在向用户显示完全没有高级内容的文档!一旦您的后端验证了用户,就提供全新的页面。虽然更安全,但此方法会耗费时间、资源和用户满意度。
通过在客户端实现高级订阅者验证和内容解密来解决这两个问题。使用此解决方案,拥有高级权限的用户将能够解密内容,而无需加载新页面或等待后端响应!
设置概述
要实现客户端解密,您将以下列方式组合对称密钥和公钥密码术
- 为每个文档创建一个随机对称密钥,为每个文档授予一个唯一密钥。
- 使用文档的对称密钥加密高级内容。
- 使用公钥加密文档密钥,使用混合加密协议来加密对称密钥。
- 使用
<amp-subscriptions>
和/或<amp-subscriptions-google>
组件,将加密的文档密钥存储在 AMP 文档内,以及加密的高级内容旁边。
AMP 文档将加密密钥存储在自身中。这可以防止加密文档与解码它的密钥分离。
它是如何工作的?
- AMP 从用户登录的文档上的加密内容中解析密钥。
- 在提供高级内容时,AMP 会将文档中的加密对称密钥作为用户授权获取的一部分发送到授权器。
- 授权器决定用户是否具有正确的权限。如果用户具有正确的权限,则授权器使用其公钥/私钥对中的授权器私钥解密文档的对称密钥。然后,授权器将文档密钥返回给 amp-subscriptions 组件逻辑。
- AMP 使用文档密钥解密高级内容并将其显示给用户!
实施步骤
按照以下步骤将 AMP 加密处理与您的内部授权服务器集成。
步骤 1:创建公钥/私钥对
要加密文档的对称密钥,您需要拥有自己的公钥/私钥对。公钥加密是一种混合加密协议,特别是带有 AES-GCM (128 位) 对称加密方法的 P-256 椭圆曲线 ECIES 非对称加密方法。
我们要求使用 Tink 使用此非对称密钥类型来完成公钥处理。要创建您的私钥-公钥对,请使用以下任一方法
- Tink 的 KeysetManager 类
- Tinkey (Tink 的密钥实用程序工具)
两者都支持密钥轮换。实施密钥轮换会限制泄露的私钥的漏洞。
为了帮助您开始创建非对称密钥,我们创建了此脚本。它
- 创建一个新的带有 AEAD 密钥的 ECIES。
- 将公钥以纯文本格式输出到输出文件。
- 将私钥输出到另一个输出文件。
- 在使用托管在 Google Cloud (GCP) 上的密钥写入输出文件之前加密生成的私钥(通常称为信封加密)。
我们需要以 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>
...
<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 的公钥是以 JSON 格式的 Tink Keyset。 有关使用此密钥集的示例,请参阅此处。
继续阅读:请参阅工作加密 AMP 文档的示例。
自动加密
使用我们的 脚本加密文档。该脚本接受 HTML 文档,并加密 <section subscriptions-section="content" encrypted>
标签内的所有内容。使用位于传递给它的 URL 的公钥,该脚本加密由该脚本创建的文档密钥。使用此脚本可确保对所有内容进行编码和格式化,以便正确提供服务。 有关使用此脚本的更多说明,请参阅此处。
步骤 3:集成授权器
当用户具有正确的授权时,您需要更新授权器以解密文档密钥。amp-subscriptions 组件会自动通过 “crypt=” URL 参数将加密的文档密钥发送到 "local"
授权器。 它执行
- 从
"local"
JSON 键字段解析文档密钥。 - 文档解密。
您必须使用 Tink 在您的授权器中解密文档密钥。要使用 Tink 解密,请使用在创建公钥/私钥对部分中生成的私钥实例化一个 HybridDecrypt 客户端。为了获得最佳性能,请在服务器启动时执行此操作。
您的 HybridDecrypt/授权器部署应大致与您的密钥轮换计划相匹配。这会使所有生成的密钥都可用于 HybridDecrypt 客户端。
Tink 在 C++、Java、Go 和 Python 中提供了大量的文档和示例,以帮助您开始进行服务器端实施。
请求管理
当请求发送到您的授权器时
- 解析授权回调 URL 中的“crypt=”参数。
- 使用 base64 解码“crypt=”参数值。存储在 URL 参数中的值是 base64 编码的加密 JSON 对象。
- 一旦加密的密钥采用其原始字节形式,请使用 HybridDecrypt 的解密函数使用您的私钥解密密钥。
- 如果解密成功,则将结果解析为 JSON 对象。
- 验证用户对 AccessRequirements JSON 字段中列出的其中一个授权的访问权限。
- 从授权响应中解密的 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。
-
作者: @CrystalOnScript