rc-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhase 0: Intent
阶段0:目标
Tell the user: "I will review your RevenueCat security posture on Android: verification mode, API keys, user identity, and server side access decisions."
告知用户:“我将审核您在Android平台上的RevenueCat安全配置,包括验证模式、API密钥、用户身份以及服务器端访问决策。”
Phase 1: Discovery
阶段1:排查
Confirm what RevenueCat already covers so you can focus on the real gaps.
| Concern | Who handles it | How |
|---|---|---|
| Receipt validation against Google Play | RevenueCat backend | Runs before |
| Purchase token reuse | RevenueCat backend | Tokens are deduplicated server side |
| Fabricated purchase tokens | RevenueCat backend | Fails Google Play verification, no entitlement granted |
| HTTPS transport to RevenueCat | SDK | Always on |
| Retry of token post on network failure | SDK | Retries on next app launch |
Ask the project these questions:
- Is set? If not,
EntitlementVerificationModeresponses are trusted without signature checking.CustomerInfo - Is the SDK configured with the public Android key (starts with ) or has someone accidentally pasted the secret key into the client?
goog_ - Are real users identified with , or is the app still relying on anonymous
Purchases.logIn(yourUserId)?$RCAnonymousID:... - Do server endpoints that serve premium content verify entitlement server side, or do they trust a client sent flag?
确认RevenueCat已覆盖的安全点,以便聚焦真正的漏洞。
| 关注点 | 负责方 | 实现方式 |
|---|---|---|
| 针对Google Play的收据验证 | RevenueCat后端 | 在 |
| 购买令牌重复使用 | RevenueCat后端 | 在服务器端对令牌进行去重 |
| 伪造购买令牌 | RevenueCat后端 | 无法通过Google Play验证,不会授予权益 |
| 与RevenueCat的HTTPS传输 | SDK | 始终启用 |
| 网络失败时重新提交令牌 | SDK | 在下次应用启动时重试 |
向项目团队询问以下问题:
- 是否设置了?如果未设置,
EntitlementVerificationMode响应将在不进行签名检查的情况下被信任。CustomerInfo - SDK是否配置了公开Android密钥(以开头),还是有人不慎将私密密钥粘贴到客户端中?
goog_ - 是否通过识别真实用户,还是应用仍依赖匿名的
Purchases.logIn(yourUserId)?$RCAnonymousID:... - 提供付费内容的服务器端点是否在服务器端验证权益,还是信任客户端发送的标记?
Phase 2: Plan
阶段2:规划
Decide each of the following before changing code.
在修改代码前确定以下各项决策。
Decision A: Entitlement verification mode
决策A:权益验证模式
| Mode | Failed verification does what | Pick when |
|---|---|---|
| No signing happens | Only for quick prototypes |
| Logged, access still granted | You want signal without risking false denials to real users |
| | You accept some false negatives from proxies or VPNs in exchange for a strict client guarantee |
| 模式 | 验证失败时的行为 | 适用场景 |
|---|---|---|
| 不执行签名验证 | 仅用于快速原型开发 |
| 记录日志,但仍授予访问权限 | 希望获取验证信号,同时避免误拒真实用户 |
| | 愿意接受因代理或VPN导致的少量误判,以换取严格的客户端安全保障 |
Decision B: Where is the authority?
决策B:权威方定位
Even with , the client is not the authority for paid content. Decide whether each premium endpoint:
ENFORCED- Calls the RevenueCat REST API per request, or
- Reads a local flag driven by RevenueCat webhooks.
has_entitlement
Option 2 is cheaper at request time, option 1 has no cache staleness. Pick one, document it, do not mix per endpoint without a reason.
即使启用模式,客户端也不是付费内容的权威方。需为每个付费端点决定:
ENFORCED- 每次请求调用RevenueCat REST API,或
- 读取由RevenueCat webhook驱动的本地标记。
has_entitlement
选项2在请求时成本更低,选项1则不存在缓存过期问题。选择其中一种方式并记录,若无特殊原因,不要在不同端点混合使用。
Decision C: User identity
决策C:用户身份管理
If the app is in production, plan to call with your own authenticated user id. Anonymous ids are device scoped identifiers, not credentials, and are shared across users of the same device.
Purchases.logIn("your_user_id")如果应用已上线,计划使用您自己的已认证用户ID调用。匿名ID是设备范围的标识符,而非凭据,会在同一设备的多个用户之间共享。
Purchases.logIn("your_user_id")Phase 3: Execute
阶段3:执行
3.1 Turn on response verification
3.1 开启响应验证
Add the mode to :
PurchasesConfigurationkotlin
PurchasesConfiguration.Builder(context, apiKey)
.entitlementVerificationMode(EntitlementVerificationMode.INFORMATIONAL)
.build()Read the verification result when you inspect entitlements:
kotlin
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 */ }
}Upgrade to once you have telemetry confirming is rare on real traffic:
ENFORCEDFAILEDkotlin
.entitlementVerificationMode(EntitlementVerificationMode.ENFORCED)In , a failed signature flips to on the affected entitlement.
ENFORCEDisActivefalse将模式添加到:
PurchasesConfigurationkotlin
PurchasesConfiguration.Builder(context, apiKey)
.entitlementVerificationMode(EntitlementVerificationMode.INFORMATIONAL)
.build()检查权益时读取验证结果:
kotlin
when (customerInfo.entitlements.verification) {
VerificationResult.VERIFIED -> { /* 响应真实可信 */ }
VerificationResult.FAILED -> { /* 可能存在篡改,记录日志并发出警报 */ }
VerificationResult.NOT_REQUESTED -> { /* 验证已禁用 */ }
VerificationResult.VERIFIED_ON_DEVICE -> { /* 已在本地验证 */ }
}当遥测数据确认真实流量中情况很少时,升级为模式:
FAILEDENFORCEDkotlin
.entitlementVerificationMode(EntitlementVerificationMode.ENFORCED)在模式下,签名失败会将受影响权益的设为。
ENFORCEDisActivefalse3.2 Keep API keys in the right place
3.2 正确存放API密钥
| Key | Where it lives | What it can do |
|---|---|---|
Public Android SDK key ( | Embedded in the app binary | Read and purchase for the calling user only |
| Secret REST API key | Your server, secret manager or env var | Full REST API, admin operations, grant entitlements |
In Android code, only the public key appears:
kotlin
Purchases.configure(
PurchasesConfiguration.Builder(context, "goog_PUBLIC_android_sdk_key").build()
)Never commit the secret key to the app repo. Grep the Android source tree for the secret key prefix and confirm zero hits before release.
| 密钥 | 存放位置 | 权限 |
|---|---|---|
公开Android SDK密钥( | 嵌入应用二进制文件 | 仅能读取当前调用用户的信息并进行购买操作 |
| 私密REST API密钥 | 您的服务器、密钥管理器或环境变量 | 拥有完整REST API权限,可执行管理操作、授予权益 |
Android代码中仅应出现公开密钥:
kotlin
Purchases.configure(
PurchasesConfiguration.Builder(context, "goog_PUBLIC_android_sdk_key").build()
)切勿将私密密钥提交到应用代码仓库。发布前,在Android源码树中搜索私密密钥前缀,确认无匹配结果。
3.3 Identify real users
3.3 识别真实用户
Call as soon as you have an authenticated user id:
logInkotlin
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfoDo not rely on the anonymous id as a credential. It is a device scoped identifier and does not protect purchase history on shared devices.
一旦获取到已认证用户ID,立即调用:
logInkotlin
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo不要依赖匿名ID作为凭据。它是设备范围的标识符,无法保护共享设备上的购买历史。
3.4 Enforce on the server, not on the client
3.4 在服务器端而非客户端强制执行
Even with mode, every server endpoint that serves paid content checks entitlement server side:
ENFORCEDpython
def get_premium_content(user_id):
info = revenuecat.get_subscriber(user_id)
if not info.entitlements["pro"].is_active:
raise Forbidden()
return contentOr read a local flag driven by RevenueCat webhooks and check that flag per request.
has_pro即使启用模式,每个提供付费内容的服务器端点都必须在服务器端验证权益:
ENFORCEDpython
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_proPhase 4: Verify
阶段4:验证
- Android source contains only the public SDK key. Secret key grep is clean.
- is set (not
EntitlementVerificationMode) and telemetry forDISABLEDis watched.VerificationResult.FAILED - Real users are identified through . Anonymous ids are only used for pre login flows.
Purchases.logIn - Every paid content endpoint on your server verifies entitlement through the REST API or a webhook driven flag. None trust a client header.
- Android源码仅包含公开SDK密钥,搜索私密密钥无结果。
- 已设置(非
EntitlementVerificationMode),并监控DISABLED的遥测数据。VerificationResult.FAILED - 通过识别真实用户,匿名ID仅用于登录前流程。
Purchases.logIn - 服务器上所有付费内容端点均通过REST API或webhook驱动的标记验证权益,无端点信任客户端头信息。
Common mistakes
常见错误
| Mistake | Why it hurts | Fix |
|---|---|---|
| Shipping the secret key in the app | An attacker who decompiles the APK can call admin REST endpoints | Public key in app, secret key only on server |
Leaving mode at | A man in the middle can forge | Set |
| Treating the anonymous id as a credential | It is not secret and is shared across users of the same device | Call |
Trusting | A tampered client can claim any entitlement | Verify on the server via REST API or webhook driven DB |
| 错误做法 | 危害 | 修复方案 |
|---|---|---|
| 在应用中打包私密密钥 | 攻击者反编译APK后可调用管理员级REST端点 | 应用中仅使用公开密钥,私密密钥仅存于服务器 |
生产环境中保留默认的 | 中间人攻击者可伪造 | 设置为 |
| 将匿名ID视为凭据 | 它并非私密信息,会在同一设备的多个用户之间共享 | 使用您的已认证用户ID调用 |
服务器端信任来自客户端的 | 被篡改的客户端可任意声称拥有某项权益 | 通过REST API或webhook驱动的数据库在服务器端验证 |