rc-security について
rc-securityは、AndroidアプリのRevenueCat購入と権限に関するセキュリティを確認するスキルです。検証モード、APIキー、ユーザーID、サーバー側のアクセス判定を点検します。
主な機能
- 01権限検証モードの確認
- 02公開鍵と秘密鍵の分離
- 03認証済みIDによるユーザー識別
- 04サーバー側で権限を確認
活用シーン
- 01リリース前のRevenueCat設定監査
- 02Androidソース内の秘密鍵を調査
- 03ログインユーザーに匿名IDを置き換え
- 04権限チェックでプレミアム機能を保護
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ revenuecat/ ai-toolkit --skill rc-securityインストール後、Claude Code で/rc-securityを入力して読み込まれたか確認します。
詳細
フェーズ 0:意図
ユーザーに次のように伝えます。「Android における RevenueCat のセキュリティ状況を確認します。確認モード、API キー、ユーザー ID、サーバー側のアクセス判定を確認します。」
フェーズ 1:調査
RevenueCat がすでに対応している内容を確認し、実際の不足点に集中できるようにします。
| 懸念事項 | 担当 | 方法 |
|---|---|---|
| Google Play に対するレシートの検証 | RevenueCat バックエンド | awaitPurchase() が返る前に実行されます |
| 購入トークンの再利用 | RevenueCat バックエンド | トークンはサーバー側で重複排除されます |
| 偽造された購入トークン | RevenueCat バックエンド | Google Play の検証に失敗し、エンタイトルメントは付与されません |
| RevenueCat への HTTPS 通信 | SDK | 常に有効です |
| ネットワーク障害時のトークン送信の再試行 | SDK | 次回のアプリ起動時に再試行されます |
プロジェクトに次の質問をします。
EntitlementVerificationModeは設定されていますか?設定されていない場合、CustomerInfoの応答は署名を確認せずに信頼されます。- SDK は公開 Android キー(
goog_で始まるもの)で設定されていますか?それとも、誰かが誤ってシークレットキーをクライアントに貼り付けていませんか? - 実ユーザーは
Purchases.logIn(yourUserId)で識別されていますか?それとも、アプリはまだ匿名の$RCAnonymousID:...に依存していますか? - プレミアムコンテンツを提供するサーバーエンドポイントは、サーバー側でエンタイトルメントを確認していますか?それとも、クライアントから送られたフラグを信頼していますか?
フェーズ 2:計画
コードを変更する前に、以下の各項目を決定します。
決定 A:エンタイトルメントの確認モード
| モード | 確認に失敗した場合の動作 | 選択する状況 |
|---|---|---|
DISABLED(デフォルト) | 署名は行われません | すばやくプロトタイプを作る場合のみ |
INFORMATIONAL | ログに記録されますが、アクセスは引き続き許可されます | 実ユーザーへの誤った拒否のリスクを負わずに状況を把握したい場合 |
ENFORCED | EntitlementInfo.isActive が false を返します | 厳格なクライアント側の保証と引き換えに、プロキシや VPN による誤検知を許容できる場合 |
決定 B:権限の決定元はどこですか?
ENFORCED であっても、有料コンテンツの権限の決定元はクライアントではありません。各プレミアムエンドポイントで、次のどちらを採用するかを決定します。
- リクエストごとに RevenueCat REST API を呼び出す、または
- RevenueCat ウェブフックによって更新されるローカルの
has_entitlementフラグを読み取る。
オプション 2 はリクエスト時のコストが低く、オプション 1 はキャッシュの古さが発生しません。どちらかを選び、文書化します。理由なくエンドポイントごとに使い分けないでください。
決定 C:ユーザー ID
アプリが本番環境で稼働している場合は、独自の認証済みユーザー 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_...) | アプリのバイナリに埋め込み | 呼び出し元のユーザーに対する読み取りと購入のみ |
| シークレット REST API キー | サーバー、シークレットマネージャー、または環境変数 | REST API 全般、管理者操作、エンタイトルメントの付与 |
Android コードには、公開キーのみを記載します。
Purchases.configure(
PurchasesConfiguration.Builder(context, "goog_PUBLIC_android_sdk_key").build()
)
シークレットキーをアプリのリポジトリにコミットしてはいけません。リリース前に、Android のソースツリーでシークレットキーのプレフィックスを grep し、一致がゼロ件であることを確認します。
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 のウェブフックによって更新されるローカルの has_pro フラグを読み取り、リクエストごとにそのフラグを確認します。
フェーズ 4:確認
- Android のソースには公開 SDK キーのみが含まれています。シークレットキーの grep で該当がないことを確認してください。
-
EntitlementVerificationModeが設定されており(DISABLEDではない)、VerificationResult.FAILEDのテレメトリーを監視しています。 - 実際のユーザーは
Purchases.logInで識別されています。匿名 ID はログイン前のフローでのみ使用してください。 - サーバー上の有料コンテンツのすべてのエンドポイントで、REST API または Webhook によるフラグを通じてエンタイトルメントを検証しています。クライアントのヘッダーを信頼しているエンドポイントはありません。
よくある間違い
| 間違い | 問題点 | 対処法 |
|---|---|---|
| アプリにシークレットキーを含めて配布する | 攻撃者が APK を逆コンパイルすると、管理用 REST エンドポイントを呼び出せます | アプリには公開キーを含め、シークレットキーはサーバーにのみ保管します |
本番環境でモードを DISABLED のままにする | 中間者攻撃によって CustomerInfo のレスポンスを偽造される可能性があります | INFORMATIONAL または ENFORCED を設定します |
| 匿名 ID を認証情報として扱う | 匿名 ID は秘密ではなく、同じデバイスを使うユーザー間で共有されます | 認証済みユーザー ID を指定して logIn を呼び出します |
サーバーでクライアントからの customerInfo を信頼する | 改ざんされたクライアントは、任意のエンタイトルメントを申告できます | REST API または Webhook による DB フラグを通じてサーバー側で検証します |