mcpField
R
rc-security

rc-security

github.com/revenuecat安全76665 次安装今天更新
代码仓库

关于 rc-security

rc-security 是一项审查 Android 应用 RevenueCat 购买与权益安全的技能,指导检查验证模式、API 密钥、用户身份和服务端访问控制。

主要功能

  1. 01检查权益验证模式
  2. 02区分公开与秘密 API 密钥
  3. 03使用已认证 ID 识别用户
  4. 04在服务端检查权益

适用场景

  1. 01发布前审查 RevenueCat 配置
  2. 02排查 Android 源码中的秘密密钥
  3. 03为登录用户替换匿名 ID
  4. 04通过权益检查保护高级内容端点

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/revenuecat/ai-toolkit --skill rc-security

安装后在 Claude Code 中输入/rc-security确认已加载。

详细介绍

阶段 0:意图

告诉用户:“我将检查你在 Android 上的 RevenueCat 安全状况:验证模式、API 密钥、用户身份以及服务端访问决策。”

阶段 1:发现

确认 RevenueCat 已经处理了哪些内容,以便聚焦真正的缺口。

关注点由谁处理方式
针对 Google Play 的收据验证RevenueCat 后端在 awaitPurchase() 返回前运行
购买令牌重复使用RevenueCat 后端在服务端对令牌进行去重
伪造的购买令牌RevenueCat 后端无法通过 Google Play 验证,不会授予权益
与 RevenueCat 之间的 HTTPS 传输SDK始终启用
网络故障时重试提交令牌SDK下次应用启动时重试

向项目提出以下问题:

  • 是否设置了 EntitlementVerificationMode?如果没有,CustomerInfo 响应会在未经签名检查的情况下被信任。
  • SDK 配置使用的是公开 Android 密钥(以 goog_ 开头),还是有人误将 Secret 密钥粘贴到了客户端?
  • 是否通过 Purchases.logIn(yourUserId) 识别真实用户,还是应用仍依赖匿名 $RCAnonymousID:...?
  • 为高级内容提供服务的服务端端点是否在服务端验证权益,还是信任客户端发送的标记?

阶段 2:计划

在更改代码前,确定以下各项。

决策 A:权益验证模式
模式验证失败时的处理方式适用情况
DISABLED(默认)不进行签名仅用于快速原型
INFORMATIONAL记录日志,仍授予访问权限希望获取信号,又不想冒误拒真实用户的风险
ENFORCEDEntitlementInfo.isActive 返回 false接受因代理或 VPN 导致的一些误判,以换取严格的客户端保证
决策 B:权威信息来源在哪里?

即使启用了 ENFORCED,客户端也不是付费内容的权威信息来源。请决定每个高级端点采用以下哪种方式:

  1. 对每个请求调用 RevenueCat REST API,或
  2. 读取由 RevenueCat Webhook 驱动的本地 has_entitlement 标记。

方式 2 在请求时成本更低,方式 1 则不会有缓存过期问题。请选择一种并记录下来,不要在没有理由的情况下为不同端点混用。

决策 C:用户身份

如果应用已投入生产,请计划使用你自己的已认证用户 ID 调用 Purchases.logIn("your_user_id")。匿名 ID 是按设备划分的标识符,不是凭据,而且同一设备上的不同用户会共享它。

阶段 3:执行

3.1 启用响应验证

将该模式添加到 PurchasesConfiguration:

PurchasesConfiguration.Builder(context, apiKey)
    .entitlementVerificationMode(EntitlementVerificationMode.INFORMATIONAL)
    .build()

检查权益时,读取验证结果:

when (customerInfo.entitlements.verification) {
    VerificationResult.VERIFIED -> { /* response is authentic */ }
    VerificationResult.FAILED -> { /* possible tampering, log and alert */ }
    VerificationResult.NOT_REQUESTED -> { /* verification disabled */ }
    VerificationResult.VERIFIED_ON_DEVICE -> { /* verified locally */ }
}

在遥测数据确认真实流量中的 FAILED 很少见后,升级到 ENFORCED:

.entitlementVerificationMode(EntitlementVerificationMode.ENFORCED)

在 ENFORCED 模式下,签名验证失败会将受影响权益的 isActive 设为 false。

3.2 将 API 密钥放在正确的位置
密钥存放位置可执行的操作
公开 Android SDK 密钥(goog_...)嵌入应用二进制文件仅为发起调用的用户读取和购买
Secret REST API 密钥你的服务端、密钥管理器或环境变量完整 REST API、管理操作、授予权益

Android 代码中只能出现公开密钥:

Purchases.configure(
    PurchasesConfiguration.Builder(context, "goog_PUBLIC_android_sdk_key").build()
)

切勿将 Secret 密钥提交到应用代码仓库。发布前,在 Android 源代码树中搜索 Secret 密钥前缀,并确认没有匹配项。

3.3 识别真实用户

一旦获得已认证的用户 ID,就调用 logIn:

val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo

不要将匿名 ID 用作凭据。它是按设备划分的标识符,无法保护共享设备上的购买历史。

3.4 在服务端而非客户端强制执行

即使启用了 ENFORCED 模式,每个为付费内容提供服务的服务端端点也都要在服务端检查权益:

def get_premium_content(user_id):
    info = revenuecat.get_subscriber(user_id)
    if not info.entitlements["pro"].is_active:
        raise Forbidden()
    return content

或者,读取由 RevenueCat Webhook 驱动的本地 has_pro 标记,并在每个请求中检查该标记。

阶段 4:验证

  • Android 源码中只包含公开 SDK 密钥。未找到密钥。
  • 已设置 EntitlementVerificationMode(不是 DISABLED),并监控 VerificationResult.FAILED 遥测数据。
  • 通过 Purchases.logIn 识别真实用户。匿名 ID 仅用于登录前的流程。
  • 服务器上的每个付费内容端点都通过 REST API 或由 webhook 驱动的标记验证权益。没有任何端点信任客户端标头。

常见错误

错误造成的影响修复方法
在应用中打包密钥攻击者反编译 APK 后,可以调用管理 REST 端点应用中使用公开密钥,密钥仅保存在服务器上
在生产环境中将模式保留为 DISABLED中间人可以伪造 CustomerInfo 响应设置为 INFORMATIONAL 或 ENFORCED
将匿名 ID 当作凭据它不是机密,同一设备的用户会共享此 ID使用经过身份验证的用户 ID 调用 logIn
在服务器上信任来自客户端的 customerInfo被篡改的客户端可以声称拥有任意权益在服务器上通过 REST API 或由 webhook 驱动的数据库进行验证

参考资料

评论

支持 Markdown