使用客户端加密保护你的订阅内容
如果你是一个在线出版物,你可能依靠订阅者来获得收入。你可能会使用 CSS 混淆(display: none
)在客户端对付费墙后的高级内容进行屏蔽。
不幸的是,更多精通技术的人可以解决这个问题。
相反,你可能会向用户显示一个完全没有高级内容的文档!一旦你的后端验证了用户,就会提供一个全新的页面。虽然这种方法更安全,但它会花费时间、资源和用户的满意度。
通过在客户端实现高级订阅者验证和内容解密来解决这两个问题。通过此解决方案,拥有高级访问权限的用户将能够解密内容,而无需加载新页面或等待后端响应!
设置概览
要实现客户端解密,你将以以下方式组合对称密钥和公钥密码术
- 为每个文档创建一个随机对称密钥,为每个文档授予一个唯一密钥。
- 使用文档的对称密钥加密高级内容。 该密钥是对称的,以允许同一个密钥加密和解密内容。
- 使用 混合加密协议使用公钥加密文档密钥,以加密对称密钥。
- 使用
<amp-subscriptions>
和/或<amp-subscriptions-google>
组件,将加密的文档密钥存储在 AMP 文档中,以及加密的高级内容。
AMP 文档本身存储加密的密钥。这可以防止加密的文档与其解码密钥分离。
它是如何工作的?
- AMP 从用户登陆的文档中解析加密内容中的密钥。
- 在提供高级内容时,AMP 将加密的对称密钥从文档发送到授权方,作为用户权限获取的一部分。
- 授权方决定用户是否拥有正确的权限。如果是,授权方将使用授权方的私钥从其公钥/私钥对中解密文档的对称密钥。然后,授权方将文档密钥返回到 amp-subscriptions 组件逻辑。
- AMP 使用文档密钥解密高级内容并将其显示给用户!
实施步骤
按照以下步骤将 AMP 加密处理与你的内部权限服务器集成。
步骤 1:创建公钥/私钥对
要加密文档的对称密钥,您需要有自己的公钥/私钥对。公钥加密是一种混合加密协议,具体来说是P-256 椭圆曲线 ECIES 非对称加密方法,以及AES-GCM(128 位)对称加密方法。
我们要求使用Tink处理公钥,方法是使用这种非对称密钥类型。要创建私钥-公钥对,请使用以下任一方法
- Tink 的KeysetManager类
- Tinkey(Tink 的密钥实用程序工具)
两者都支持密钥轮换。实现密钥轮换可以限制被盗用私钥造成的漏洞。
为了帮助您开始创建非对称密钥,我们创建了此脚本。它
- 使用 AEAD 密钥创建新的 ECIES。
- 将公钥以明文形式输出到输出文件。
- 将私钥输出到另一个输出文件。
- 在写入输出文件之前,使用托管在 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"
授权者。它执行以下操作:
- 从
"local"
JSON 密钥字段解析文档密钥。 - 文档解密。
您必须在授权者中使用 Tink 来解密文档密钥。要使用 Tink 进行解密,请使用在创建公钥/私钥对部分中生成的私钥实例化HybridDecrypt客户端。为了获得最佳性能,请在服务器启动时执行此操作。
您的 HybridDecrypt/Authorizer 部署应大致匹配您的密钥轮换计划。这会为 HybridDecrypt 客户端创建所有已生成密钥的可用性。
Tink 在 C++、Java、Go 和 Python 中提供了广泛的文档和示例,以帮助您开始进行服务器端实施。
请求管理
当请求到达您的授权器时
- 解析“crypt=”参数的授权 pingback URL。
- 使用 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 问题。
-
由 @CrystalOnScript 撰写