使用客户端加密保护您的订阅内容
重要提示:此文档不适用于您当前选择的格式 电子邮件!
如果您是一家在线出版物,您可能依赖订阅者获取收入。您可以使用 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) 上的密钥加密生成的私钥,然后再写入输出文件(通常称为 信封加密)。
我们要求将您的公共 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 格式。有关使用此密钥集的示例,请参阅 此处。
自动加密
使用我们的 脚本加密文档。该脚本接受 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 的 decrypt 函数,使用您的私钥解密密钥。
- 如果解密成功,请将结果解析为 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 问题。
-
由 @CrystalOnScript 编写