アプリへのクライアントサイド ライセンス確認の追加

警告: アプリでのライセンス確認処理をクライアント側で実施すると、攻撃者が簡単に確認処理関連ロジックを変更または削除できるようになります。

そのため、代わりにサーバーサイド ライセンス確認を実施することを強くおすすめします。

パブリッシャー アカウントと開発環境をセットアップしたら(ライセンス機能追加のためのセットアップを参照)、License Verification Library(LVL)を使用してアプリにライセンス確認機能を追加できます。

LVL を使用してライセンス確認機能を追加するには、以下のタスクを行います。

  1. アプリ マニフェストへのライセンス権限の追加
  2. Policy の実装(LVL に用意されている実装から選択することも、独自に作成することもできます)。
  3. Obfuscator の実装Policy によりライセンス応答データがキャッシュに保存される場合)。
  4. アプリのメイン アクティビティへのライセンスをチェックするコードの追加
  5. DeviceLimiter の実装(必須ではなく、ほとんどのアプリケーションでは推奨されません)。

下記のセクションでは、これらのタスクについて説明します。この統合作業が完了すると、アプリを正常にコンパイルし、(テスト環境のセットアップで説明されているように)テストを開始できるようになります。

LVL に含まれるソースファイル全体の概要については、LVL のクラスとインターフェースの概要をご覧ください。

ライセンス権限の追加

Google Play アプリを使用してライセンス チェックをサーバーに送信するには、アプリで適切な権限(com.android.vending.CHECK_LICENSE)をリクエストする必要があります。アプリでライセンス権限を宣言せずにライセンス チェックを開始しようとすると、LVL からセキュリティ例外がスローされます。

アプリでライセンス権限をリクエストするには、次のように、<uses-permission> 要素を <manifest> の子要素として宣言します。

<uses-permission android:name="com.android.vending.CHECK_LICENSE" />

例として、LVL サンプルアプリでの権限の宣言方法を以下に示します。

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...">
    <!-- Devices >= 3 have version of Google Play that supports licensing. -->
    <uses-sdk android:minSdkVersion="3" />
    <!-- Required permission to check licensing. -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />
    ...
</manifest>

注: 現時点では、LVL ライブラリ プロジェクトのマニフェストの中で CHECK_LICENSE 権限を宣言することはできません。このマニフェストは、SDK Tools によって依存アプリのマニフェストにマージされないためです。権限は、各依存アプリのマニフェストで宣言する必要があります。

Policy の実装

特定のライセンスを持つ特定のユーザーにアプリの使用を許すかどうかを決めるのは、Google Play ライセンス サービス自体ではありません。 その役割は、アプリで用意する Policy 実装が担います。

Policy とは、LVL で宣言されるインターフェースで、ライセンス チェックの結果に基づいてユーザーにアプリの使用を許可または拒否するロジックを保持するためのものです。LVL を使用するには、アプリで Policy の実装を用意する必要があります。

Policy インターフェースでは、allowAccess()processServerResponse() の 2 つのメソッドを宣言します。これらは、ライセンス サーバーからの応答を処理するときに、LicenseChecker インスタンスによって呼び出されます。また、LicenseResponse という列挙型も宣言します。これは、processServerResponse() の呼び出しで渡されるライセンス応答値を指定するものです。

  • processServerResponse() により、使用を許可するかどうかを決定する前に、ライセンス サーバーから受信した生の応答データを前処理できます。

    一般的な実装では、ライセンス応答から一部またはすべての項目を抽出して、そのデータを(SharedPreferences ストレージなどを使って)永続ストアにローカルに保存します。こうすることで、アプリを再起動したり、デバイスの電源を入れ直したりしても、そのデータを確実に利用できます。たとえば、Policy によっては、アプリが起動されるたびに値をリセットするのではなく、最後に成功したライセンス チェックのタイムスタンプ、再試行カウント、ライセンス有効期間などの情報を、永続ストア内に維持しておきます。

    応答データをローカルに保存する場合は、必ず Policy でデータを難読化する必要があります(下記の Obfuscator の実装を参照)。

  • allowAccess() では、ライセンス サーバーやキャッシュから取得できるライセンス応答データ、または他のアプリ固有の情報に基づいて、ユーザーにアプリの使用を許可するかどうかを決定します。たとえば、allowAccess() の実装では、使用状況など、バックエンド サーバーから取得できるデータに基づいて、追加の条件を設けることもできます。どのようなケースでも、allowAccess() の実装では、ユーザーがアプリ使用のライセンスを付与されている場合(ライセンス サーバーでそう判断された場合)、またはネットワークやシステムの一時的な問題でライセンス チェックを完了できない場合にのみ、true を返すようにします。そのような場合、実装では次にライセンス チェックが完了するまでの間、再試行応答のカウントを保持しながら暫定的に使用を許可することもできます。

アプリにライセンス機能を追加するプロセスを簡素化し、Policy の設計方法をわかりやすく示すために、LVL には次の 2 種類の完全な Policy 実装が含まれています。これらは、そのまま使用することも、ニーズに合うよう変更して使用することもできます。

  • ServerManagedPolicy - サーバーから提供される設定とキャッシュに保存された応答を使用して、さまざまなネットワーク条件下で使用可否を管理できる柔軟な Policy
  • StrictPolicy - 応答データをキャッシュに保存せず、ライセンス付与済みという応答がサーバーから返された場合にのみ使用を許可するポリシー。

ほとんどのアプリでは、ServerManagedPolicy を使用することを強くおすすめします。ServerManagedPolicy は LVL のデフォルトであり、LVL サンプルアプリと統合されています。

カスタム ポリシーのガイドライン

ライセンス機能の実装では、LVL で提供される完全なポリシー(ServerManagedPolicy と StrictPolicy)のいずれかを使用することも、カスタム ポリシーを作成することもできます。どのようなカスタム ポリシーを実装する場合でも、そのために理解し、考慮しなければならない重要な設計ポイントがいくつかあります。

ライセンス サーバーでは、サービス拒否の原因になるようなリソースの過剰使用を防ぐため、一般的なリクエスト制限が適用されます。アプリからのリクエストがこの制限を超えると、ライセンス サーバーから 503 応答が返され、一般的なサーバーエラーとしてアプリに渡されます。つまり、ユーザーは制限がリセットされるまでライセンス応答を利用できないことになり、無期限に影響を受ける可能性があります。

カスタム ポリシーの設計にあたっては、Policy について以下をおすすめします。

  1. 最後に正常に取得したライセンス応答をローカルの永続ストレージにキャッシュとして保存(および適切に難読化)すること。
  2. すべてのライセンス チェックに対して、キャッシュに保存された応答が有効である限り、ライセンス サーバーへのリクエストは行わずキャッシュに保存された応答を返すこと。 サーバー提供の VT 付加情報に従って応答の有効性を設定することを強くおすすめします。詳細については、サーバー応答の付加情報をご覧ください。
  3. リクエストの再試行でエラーが発生した場合は、指数バックオフ期間を使用すること。失敗したリクエストは、Google Play クライアントによって自動的に再試行されるため、ほとんどの場合は、Policy によるリクエストの再試行は不要です。
  4. ライセンス チェックの再試行中は、ユーザーが限られた時間または使用回数でアプリを使用できるようにする「猶予期間」を提供すること。猶予期間を設けることで、次にライセンス チェックが成功するまでの間、ユーザーにはアプリを使用できるというメリットがあり、アプリには、有効なライセンス応答がない場合にユーザーに使用限度を課すことができるというメリットがあります。

上記のガイドラインに沿って Policy を設計することは重要です。そうすることで、エラーが発生しても、アプリを効果的に制御しながら、できるだけ高いユーザー エクスペリエンスを確保できるからです。

なお、どの Policy でも、ライセンス サーバー提供の設定を使用して、有効性、キャッシュ保存、再試行猶予期間などを管理できます。サーバー提供の設定は簡単に抽出できるため、利用することを強くおすすめします。付加情報の抽出方法と使用方法の例については、ServerManagedPolicy の実装をご覧ください。サーバー設定の一覧と使用方法については、サーバー応答の付加情報をご覧ください。

ServerManagedPolicy

LVL には、ServerManagedPolicy と呼ばれる、Policy インターフェースの完全な実装として推奨のものが含まれています。この実装は LVL クラスと統合され、ライブラリのデフォルト Policy として機能します。

ServerManagedPolicy には、ライセンスと再試行応答のすべての処理が含まれています。すべての応答データは SharedPreferences ファイルにローカルにキャッシュ保存され、アプリの Obfuscator 実装により難読化されます。これにより、ライセンス応答データは安全に保持され、またデバイスの電源を入れ直してもなくなりません。ServerManagedPolicy には、インターフェース メソッド processServerResponse() および allowAccess() の具体的な実装が用意されているほか、ライセンス応答の管理をサポートする一連のメソッドと型も含まれています。

ServerManagedPolicy の大きな特長は、サーバー提供の設定を使用できることで、この点は重要です。サーバー提供の設定は、アプリの払い戻し期間を通して、またさまざまなネットワーク条件やエラー条件にわたって、ライセンスを管理するためのよりどころとして使用されます。ライセンス チェックのためにアプリから Google Play サーバーにアクセスすると、サーバーにより、特定のライセンス応答タイプの付加情報フィールドに Key-Value ペアとしていくつかの設定が追加されます。たとえば、サーバーからは、アプリのライセンス有効期間、再試行猶予期間、最大許容再試行回数などの推奨値が提供されます。ServerManagedPolicy では、processServerResponse() メソッドでライセンス応答から値を抽出し、allowAccess() メソッドでその値をチェックしています。ServerManagedPolicy で使用しているサーバー提供の設定の一覧については、サーバー応答の付加情報をご覧ください。

高い利便性、優れたパフォーマンス、そして Google Play サーバーのライセンス設定を利用できるというメリットがあるため、ライセンス Policy として ServerManagedPolicy を使用することを強くおすすめします

SharedPreferences にローカルに保存されるライセンス応答データのセキュリティが懸念される場合は、強力な難読化アルゴリズムを使用するか、ライセンス データを保存しない厳格な Policy を設計します。LVL には、そのような Policy の例が含まれています。詳細については、StrictPolicy をご覧ください。

ServerManagedPolicy は、アクティビティにインポートしてインスタンスを作成し、そのインスタンスへの参照を LicenseChecker 作成時に渡すだけで使用できます。詳細については、LicenseChecker と LicenseCheckerCallback のインスタンス化をご覧ください。

StrictPolicy

LVL には、StrictPolicy と呼ばれるもう一つの Policy インターフェースの完全な実装が含まれています。StrictPolicy 実装では、ユーザーがアプリにアクセスしたときに、ライセンスが付与されていることを示すライセンス応答がサーバーから届かない限り、アプリの使用は許可されません。その点で、ServerManagedPolicy よりも制限の厳しいポリシーとなっています。

StrictPolicy の主な特長は、いかなるライセンス応答データも永続ストアにローカルに保存されないことです。データが保存されないため、再試行リクエストは追跡されず、キャッシュに保存された応答を使用してのライセンス チェックは行えません。この Policy では、次の場合にのみ使用が許可されます。

  • ライセンス応答がライセンス サーバーから届き、かつ、
  • そのライセンス応答が、ユーザーにアプリを使用するためのライセンスが付与されていることを示している

StrictPolicy を使用することが適しているのは、ユーザーがアプリを使用する際にライセンスが付与されていることを確認できない限り、どのような状況でもアプリの使用を許可しないようにする場合です。また、この Policy は、ServerManagedPolicy よりもセキュリティが少し高くなっています。ローカルにキャッシュ保存されたデータがなく、悪意のあるユーザーがそれを改ざんしてアプリを使用する可能性がないためです。

その一方で、この Policy は、ネットワーク(モバイルまたは Wi-Fi)接続が利用不可の状況ではアプリを使用できないという点で、通常のユーザーにとっては不便です。もう一つの副作用として、キャッシュ保存された応答を利用できないため、アプリではより多くのライセンス チェック リクエストをサーバーに送信する必要があります。

全体として見ると、このポリシーは、厳格なセキュリティとアクセス制御のために、ユーザーの利便性をある程度犠牲にしていると言えます。この Policy の使用にあたっては、そのバランスが適切かどうかを慎重に検討してください。

StrictPolicy は、アクティビティにインポートしてインスタンスを作成し、そのインスタンスへの参照を LicenseChecker 作成時に渡すだけで使用できます。詳細については、LicenseChecker と LicenseCheckerCallback のインスタンス化をご覧ください。

一般的な Policy 実装では、アプリのライセンス応答を永続ストアにローカルに保存する必要があります。こうすることで、アプリを再起動したり、デバイスの電源を入れ直したりした場合でも、そのデータを利用できます。たとえば、Policy によっては、アプリが起動されるたびに値をリセットするのではなく、最後に成功したライセンス チェックのタイムスタンプ、再試行カウント、ライセンス有効期間などの情報を、永続ストア内に維持しておきます。LVL に含まれるデフォルト Policy の ServerManagedPolicy では、ライセンス応答データは SharedPreferences インスタンスに保存され、永続的に保持されます。

Policy では、保存されたライセンス応答データに基づいてアプリの使用可否が決定されるため、保存されたデータの安全性を確保し、デバイスの root ユーザーによる再利用や操作がなされないようにする必要があります。具体的には、Policy では、アプリとデバイスに固有の鍵を使用して、データを保存する前に常に難読化する必要があります。難読化したデータのアプリ間やデバイス間での共有を防ぐため、アプリ固有かつデバイス固有の鍵で難読化することが重要です。

LVL は、アプリでライセンス応答データを安全かつ永続的に保存する場合に役立ちます。まず、Obfuscator インターフェースが用意されています。これにより、保存データ用に選択した難読化アルゴリズムをアプリに実装できます。さらに、LVL には PreferenceObfuscator ヘルパークラスが用意されています。これを使用すれば、アプリの Obfuscator クラスを呼び出す作業や、SharedPreferences インスタンス内の難読化データを読み書きする作業のほとんどを処理できます。

LVL には、AESObfuscator と呼ばれる、AES 暗号化を使用してデータを難読化する Obfuscator の完全な実装が含まれています。AESObfuscator は、アプリでそのまま使用することも、ニーズに合うよう変更して使用することもできます。ライセンス応答データをキャッシュに保存する Policy(ServerManagedPolicy など)を使用している場合は、AESObfuscator を Obfuscator 実装のベースにすることを強くおすすめします。詳細については、次のセクションをご覧ください。

AESObfuscator

LVL には、AESObfuscator と呼ばれる、Obfuscator インターフェースの完全な実装として推奨のものが含まれています。この実装は LVL サンプルアプリと統合され、ライブラリのデフォルト Obfuscator として機能します。

AESObfuscator では、ストレージに対して読み書きする際のデータの暗号化と復号に AES を使用することで、データの安全な難読化を実現しています。Obfuscator の暗号化シードは、アプリで提供する次の 3 つのデータ項目から生成されます。

  1. ソルト - 難読化(解除)ごとに使用されるランダムなバイト配列。
  2. アプリ ID の文字列。通常はアプリのパッケージ名。
  3. デバイス ID の文字列。一意になるよう、できるだけ多くのデバイス固有のソースから生成される。

AESObfuscator を使用するには、まずアクティビティにインポートします。そして、ソルトのバイト値を格納するための private static final の配列を宣言し、ランダムに生成された 20 個のバイト値で初期化します。

Kotlin

// Generate 20 random bytes, and put them here.
private val SALT = byteArrayOf(
        -46, 65, 30, -128, -103, -57, 74, -64, 51, 88,
        -95, -45, 77, -117, -36, -113, -11, 32, -64, 89
)

Java

...
    // Generate 20 random bytes, and put them here.
    private static final byte[] SALT = new byte[] {
     -46, 65, 30, -128, -103, -57, 74, -64, 51, 88, -95,
     -45, 77, -117, -36, -113, -11, 32, -64, 89
     };
    ...

次に、デバイス ID を格納する変数を宣言し、その値を適切な方法で生成します。たとえば、LVL に含まれるサンプルアプリでは、各デバイスに固有の android.Settings.Secure.ANDROID_ID のシステム設定を照会しています。

使用する API によっては、デバイス固有の情報を取得するために、アプリで追加の権限をリクエストしなければならない場合があります。たとえば、TelephonyManager に照会してデバイスの IMEI または関連データを取得するには、アプリのマニフェストで android.permission.READ_PHONE_STATE 権限もリクエストする必要があります。

Obfuscator で使用するデバイス固有の情報を取得するためだけに新しい権限をリクエストする場合は、事前にそれがアプリや Google Play のフィルタに与える影響を考慮してください(権限によっては、SDK ビルドツールによって関連する <uses-feature> が追加されるため)。

最後に、ソルト、アプリ ID、デバイス ID を渡して、AESObfuscator のインスタンスを作成します。PolicyLicenseChecker を作成する際に、同時にインスタンスを作成できます。次に例を示します。

Kotlin

    ...
    // Construct the LicenseChecker with a Policy.
    private val checker = LicenseChecker(
            this,
            ServerManagedPolicy(this, AESObfuscator(SALT, packageName, deviceId)),
            BASE64_PUBLIC_KEY
    )
    ...

Java

    ...
    // Construct the LicenseChecker with a Policy.
    checker = new LicenseChecker(
        this, new ServerManagedPolicy(this,
            new AESObfuscator(SALT, getPackageName(), deviceId)),
        BASE64_PUBLIC_KEY // Your public licensing key.
        );
    ...

完全な例については、LVL サンプルアプリの MainActivity をご覧ください。

アクティビティからのライセンス チェック

アプリの使用を管理するための Policy を実装したら、次のステップはアプリにライセンス チェックを追加することです。ライセンス チェックでは、必要に応じてライセンス サーバーへのクエリを開始し、そのライセンス応答に基づいてアプリの使用を管理します。ライセンス チェックを追加して応答を処理する作業は、すべてメインの Activity のソースファイルで行います。

ライセンス チェックを追加して応答を処理するには、以下を行う必要があります。

  1. インポートの追加
  2. LicenseCheckerCallback のプライベート内部クラスとしての実装
  3. LicenseCheckerCallback から UI スレッドに送信するためのハンドラの作成
  4. LicenseChecker と LicenseCheckerCallback のインスタンス化
  5. ライセンス チェックを開始するための checkAccess() の呼び出し
  6. ライセンス用公開鍵の埋め込み
  7. LicenseChecker の onDestroy() メソッドの呼び出しによる IPC 接続の終了

下記のセクションでは、これらのタスクについて説明します。

ライセンス チェックと応答の概要

通常、アプリのメイン Activity へのライセンス チェックの追加は、onCreate() メソッド内で行います。そうすることで、ユーザーがアプリを直接起動したときに、ライセンス チェックがすぐに呼び出されます。状況によっては、他の場所にライセンス チェックを追加する場合もあります。たとえば、アプリに複数の Activity コンポーネントが含まれていて、それらが他のアプリから Intent で起動される場合は、そのような Activity にもライセンス チェックを追加します。

ライセンス チェックは、次の 2 つの主要な操作で構成されます。

  • ライセンス チェックを開始するメソッドの呼び出し。LVL では、これはアプリで作成する LicenseChecker オブジェクトの checkAccess() メソッドの呼び出しです。
  • ライセンス チェックの結果を返すコールバック。LVL では、これはアプリで実装する LicenseCheckerCallback インターフェースです。このインターフェースでは、ライセンス チェックの結果に基づいてライブラリから呼び出される allow()dontAllow() の 2 つのメソッドを宣言します。これら 2 つのメソッドには、ユーザーによるアプリの使用を許可または拒否するために必要なロジックをすべて実装します。ただし、これらのメソッドで使用の可否を決定するのではないことに注意してください。それを決定するのは Policy 実装の役割です。これらのメソッドでは、使用許可と使用拒否(およびエラー処理)の方法について、アプリの動作を規定するだけです。

    allow() メソッドと dontAllow() メソッドからは、応答の「reason(理由)」も返されます。これは PolicyLICENSEDNOT_LICENSEDRETRY のいずれかです。特に、dontAllow() の応答が RETRY の場合には対処が必要で、「再試行」ボタンをユーザーに表示する必要があります。こうしたケースは、リクエスト中にサービスを利用できなかったことが原因で発生する可能性があります。

図 1. 一般的なライセンス チェック動作の概要

上の図は、一般的なライセンス チェックの動作(下記)を示しています。

  1. アプリのメイン Activity のコードにより、LicenseCheckerCallback オブジェクトと LicenseChecker オブジェクトがインスタンス化されます。LicenseChecker 作成時に、コードから引数として Context、使用する Policy 実装、パブリッシャー アカウントのライセンス用公開鍵が渡されます。
  2. 次に、コードから LicenseChecker オブジェクトの checkAccess() メソッドが呼び出されます。そのメソッド実装によって Policy が呼び出され、SharedPreferences にローカルにキャッシュ保存された有効なライセンス応答があるかどうかが判断されます。
    • ある場合は、checkAccess() 実装により allow() が呼び出されます。
    • ない場合は、LicenseChecker によってライセンス チェック リクエストが開始され、リクエストがライセンス サーバーに送信されます。

    注: ドラフトアプリのライセンス チェックを実行すると、ライセンス サーバーからは常に LICENSED が返されます。

  3. 応答を受信すると、LicenseChecker は、署名付きライセンスデータを検証する LicenseValidator を作成して応答のフィールドを抽出し、それらを Policy に渡してさらに評価を行います。
    • ライセンスが有効な場合は、Policy によってその応答が SharedPreferences にキャッシュ保存され、バリデータに通知されます。その後、バリデータによって LicenseCheckerCallback オブジェクトの allow() メソッドが呼び出されます。
    • ライセンスが有効でない場合は、Policy からバリデータに通知され、バリデータによって LicenseCheckerCallbackdontAllow() メソッドが呼び出されます。
  4. 回復可能なローカルエラーまたはサーバーエラー(リクエストを送信するためのネットワークが利用できないなど)が発生した場合は、LicenseChecker から Policy オブジェクトの processServerResponse() メソッドに RETRY 応答が渡されます。

    また、コールバック メソッド allow()dontAllow() のいずれも reason 引数を受け取ります。通常、allow() メソッドの reason は Policy.LICENSEDPolicy.RETRY であり、dontAllow() メソッドの reason は Policy.NOT_LICENSEDPolicy.RETRY です。これらの応答値は、dontAllow()Policy.RETRY が返された(おそらくライセンス サービスが利用できない)場合に「再試行」ボタンを提示するなど、ユーザーに適切な応答を返すために役立ちます。

  5. アプリエラーが発生した場合(アプリで無効なパッケージ名のライセンスをチェックしようとした場合など)、LicenseChecker から LicenseCheckerCallback の applicationError() メソッドにエラー応答が渡されます。

なお、アプリでは、ライセンス チェックの開始と結果の処理(下記セクションで説明)に加え、Policy 実装と、その Policy で応答データを保存する場合(ServerManagedPolicy など)には Obfuscator 実装も必要になります。

インポートの追加

まず、アプリのメイン Activity のクラスファイルを開き、LVL パッケージから LicenseCheckerLicenseCheckerCallback をインポートします。

Kotlin

import com.google.android.vending.licensing.LicenseChecker
import com.google.android.vending.licensing.LicenseCheckerCallback

Java

import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;

LVL が提供するデフォルトの Policy 実装(ServerManagedPolicy)を使用する場合は、それを AESObfuscator とともにインポートします。カスタムの PolicyObfuscator を使用する場合は、代わりにそれらをインポートします。

Kotlin

import com.google.android.vending.licensing.ServerManagedPolicy
import com.google.android.vending.licensing.AESObfuscator

Java

import com.google.android.vending.licensing.ServerManagedPolicy;
import com.google.android.vending.licensing.AESObfuscator;

LicenseCheckerCallback のプライベート内部クラスとしての実装

LicenseCheckerCallback は、ライセンス チェックの結果を処理するための、LVL が提供するインターフェースです。LVL を利用したライセンス機能をサポートするには、アプリの使用を許可または拒否するために、LicenseCheckerCallback とそのメソッドを実装する必要があります。

ライセンス チェックの結果として、常に LicenseCheckerCallback メソッドのいずれかが呼び出されます。この呼び出しは、応答ペイロード、サーバー応答コード自体、Policy による追加処理の検証に基づいて行われます。アプリでは、状況に応じた方法でメソッドを実装できます。一般的には、メソッドは UI の状態とアプリの使用を管理するためのものに限定し、シンプルに保つのが最善です。その他のライセンス応答の処理(バックエンド サーバーへの接続やカスタム制約の適用など)を追加する場合は、そのコードを LicenseCheckerCallback メソッドに入れる代わりに、Policy に組み込むことを検討します。

ほとんどの場合、LicenseCheckerCallback の実装は、アプリのメイン Activity クラス内のプライベート クラスとして宣言します。

必要に応じて allow() メソッドと dontAllow() メソッドを実装します。これらのメソッドでは、最初は単純な結果処理操作(ライセンスの結果をダイアログ表示するなど)を行うようにします。そうすることで、アプリを早期に実行でき、デバッグも容易に行えます。その後、必要な動作が正確に行われることを確認できたら、より複雑な処理を追加するようにします。

dontAllow() でライセンスなしの応答を処理する際の推奨事項を以下に示します。

  • 返された reasonPolicy.RETRY の場合は、ライセンス チェックを新たに開始するためのボタンを含む「再試行」ダイアログをユーザーに表示します。
  • 「アプリの購入」ダイアログを表示します。このダイアログには、ユーザーがそのアプリを購入できる Google Play の詳細ページにディープリンクされたボタンを表示します。このようなリンクの設定方法の詳細については、Google Play へのリンクをご覧ください。
  • ライセンスが付与されていないためアプリの機能が制限される旨のトースト通知を表示します。

次の例は、LVL サンプルアプリでの LicenseCheckerCallback の実装方法を示しています。ライセンス チェックの結果をダイアログに表示するメソッドも使用されています。

Kotlin

private inner class MyLicenseCheckerCallback : LicenseCheckerCallback {

    override fun allow(reason: Int) {
        if (isFinishing) {
            // Don't update UI if Activity is finishing.
            return
        }
        // Should allow user access.
        displayResult(getString(R.string.allow))
    }

    override fun dontAllow(reason: Int) {
        if (isFinishing) {
            // Don't update UI if Activity is finishing.
            return
        }
        displayResult(getString(R.string.dont_allow))

        if (reason == Policy.RETRY) {
            // If the reason received from the policy is RETRY, it was probably
            // due to a loss of connection with the service, so we should give the
            // user a chance to retry. So show a dialog to retry.
            showDialog(DIALOG_RETRY)
        } else {
            // Otherwise, the user isn't licensed to use this app.
            // Your response should always inform the user that the application
            // isn't licensed, but your behavior at that point can vary. You might
            // provide the user a limited access version of your app or you can
            // take them to Google Play to purchase the app.
            showDialog(DIALOG_GOTOMARKET)
        }
    }
}

Java

private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
    public void allow(int reason) {
        if (isFinishing()) {
            // Don't update UI if Activity is finishing.
            return;
        }
        // Should allow user access.
        displayResult(getString(R.string.allow));
    }

    public void dontAllow(int reason) {
        if (isFinishing()) {
            // Don't update UI if Activity is finishing.
            return;
        }
        displayResult(getString(R.string.dont_allow));

        if (reason == Policy.RETRY) {
            // If the reason received from the policy is RETRY, it was probably
            // due to a loss of connection with the service, so we should give the
            // user a chance to retry. So show a dialog to retry.
            showDialog(DIALOG_RETRY);
        } else {
            // Otherwise, the user isn't licensed to use this app.
            // Your response should always inform the user that the application
            // isn't licensed, but your behavior at that point can vary. You might
            // provide the user a limited access version of your app or you can
            // take them to Google Play to purchase the app.
            showDialog(DIALOG_GOTOMARKET);
        }
    }
}

さらに、applicationError() メソッドを実装します。これは、再試行できないエラーをアプリで処理できるように LVL から呼び出されるメソッドです。このようなエラーの一覧については、ライセンス リファレンスサーバー応答コードをご覧ください。このメソッドは、状況に応じた方法で実装できます。ほとんどの場合、このメソッドではエラーコードを記録して dontAllow() を呼び出します。

LicenseCheckerCallback から UI スレッドに送信するためのハンドラの作成

ライセンス チェックの間、リクエストは LVL から Google Play アプリに引き渡され、Google Play アプリではライセンス サーバーとの通信処理が行われます。LVL によるリクエストの引き渡しは非同期 IPC(Binder を使用)を介して行われるため、実際の処理とネットワーク通信は、アプリで管理するスレッドでは行われません。同様に、結果を受け取った Google Play アプリからのコールバック メソッドの呼び出しも、IPC を介して行われます。その後、このコールバック メソッドは、アプリプロセス内の IPC スレッドプールで実行されます。

アプリと Google Play アプリとの間の IPC 通信(リクエストを送信する呼び出しや応答を受信するコールバックなど)は、LicenseChecker クラスで管理します。オープン ライセンス リクエストの追跡やそのタイムアウトの管理も LicenseChecker で行います。

LicenseChecker では、タイムアウトを適切に処理し、アプリの UI スレッドに影響を与えずに着信応答を処理できるよう、インスタンス化時にバックグラウンド スレッドを生成します。ライセンス チェック結果に対するすべての処理(その結果がサーバーから受信した応答か、タイムアウト エラーかにかかわらず)は、このスレッドで行います。処理の終わりに、LVL によってバックグラウンド スレッドから LicenseCheckerCallback メソッドが呼び出されます。

これは、アプリにとって以下を意味します。

  1. 多くの場合、LicenseCheckerCallback メソッドはバックグラウンド スレッドから呼び出されます。
  2. UI スレッドに Handler を作成してコールバック メソッドにその Handler への送信をさせない限り、このようなメソッドでは UI スレッドで状態を更新したり、処理を起動したりすることはできません。

LicenseCheckerCallback メソッドで UI スレッドを更新する場合は、下記に示すように、メイン Activity の onCreate() メソッド内で Handler をインスタンス化します。この例では、LVL サンプルアプリの LicenseCheckerCallback メソッド(上記参照)で displayResult() を呼び出し、Handler の post() メソッドを介して UI スレッドを更新しています。

Kotlin

    private lateinit var handler: Handler

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        handler = Handler()
    }

Java

    private Handler handler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        handler = new Handler();
    }

これで、LicenseCheckerCallback メソッド内で Handler メソッドを使用して、Runnable または Message オブジェクトを Handler に送信できます。以下に、LVL に含まれているサンプルアプリで、どのように Runnable を UI スレッドの Handler に送信し、ライセンス ステータスを表示しているかを示します。

Kotlin

private fun displayResult(result: String) {
    handler.post {
        statusText.text = result
        setProgressBarIndeterminateVisibility(false)
        checkLicenseButton.isEnabled = true
    }
}

Java

private void displayResult(final String result) {
        handler.post(new Runnable() {
            public void run() {
                statusText.setText(result);
                setProgressBarIndeterminateVisibility(false);
                checkLicenseButton.setEnabled(true);
            }
        });
    }

LicenseChecker と LicenseCheckerCallback のインスタンス化

メイン Activity の onCreate() メソッド内で、LicenseCheckerCallback と LicenseChecker のプライベート インスタンスを作成します。LicenseChecker のコンストラクタを呼び出すときには LicenseCheckerCallback のインスタンスへの参照を渡さなければならないため、最初に LicenseCheckerCallback をインスタンス化する必要があります。

LicenseChecker をインスタンス化する際には、次のパラメータを渡す必要があります。

  • アプリの Context
  • ライセンス チェックに使用する Policy 実装への参照。通常、LVL が提供するデフォルトの Policy 実装(ServerManagedPolicy)を使用します。
  • パブリッシャー アカウントのライセンス用公開鍵を格納した文字列変数。

ServerManagedPolicy を使用する場合は、クラスに直接アクセスする必要はなく、下記の例に示すように、LicenseChecker コンストラクタでインスタンス化できます。ServerManagedPolicy を作成する際には、新しい Obfuscator インスタンスへの参照を渡す必要があります。

下記の例では、Activity クラスの onCreate() メソッドから LicenseCheckerLicenseCheckerCallback をインスタンス化しています。

Kotlin

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var licenseCheckerCallback: LicenseCheckerCallback
    private lateinit var checker: LicenseChecker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Construct the LicenseCheckerCallback. The library calls this when done.
        licenseCheckerCallback = MyLicenseCheckerCallback()

        // Construct the LicenseChecker with a Policy.
        checker = LicenseChecker(
                this,
                ServerManagedPolicy(this, AESObfuscator(SALT, packageName, deviceId)),
                BASE64_PUBLIC_KEY // Your public licensing key.
        )
        ...
    }
}

Java

public class MainActivity extends Activity {
    ...
    private LicenseCheckerCallback licenseCheckerCallback;
    private LicenseChecker checker;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Construct the LicenseCheckerCallback. The library calls this when done.
        licenseCheckerCallback = new MyLicenseCheckerCallback();

        // Construct the LicenseChecker with a Policy.
        checker = new LicenseChecker(
            this, new ServerManagedPolicy(this,
                new AESObfuscator(SALT, getPackageName(), deviceId)),
            BASE64_PUBLIC_KEY // Your public licensing key.
            );
        ...
    }
}

なお、LicenseChecker により UI スレッドから LicenseCheckerCallback メソッドが呼び出されるのは、ローカルにキャッシュ保存された有効なライセンス応答がある場合のみです。ライセンス チェックがサーバーに送信された場合は、コールバックは常に(ネットワーク エラーの場合も含め)バックグラウンド スレッドから開始されます。

ライセンス チェックを開始するための checkAccess() の呼び出し

メイン Activity に、LicenseChecker インスタンスの checkAccess() メソッドの呼び出しを追加します。この呼び出しでは、パラメータとして LicenseCheckerCallback インスタンスへの参照を渡します。呼び出しの前に特別な UI 効果や状態管理を処理する必要がある場合は、ラッパー メソッドから checkAccess() を呼び出すと便利な場合があります。たとえば、LVL のサンプルアプリでは、次のように、doCheck() ラッパー メソッドから checkAccess() を呼び出しています。

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Call a wrapper method that initiates the license check
        doCheck()
        ...
    }
    ...
    private fun doCheck() {
        checkLicenseButton.isEnabled = false
        setProgressBarIndeterminateVisibility(true)
        statusText.setText(R.string.checking_license)
        checker.checkAccess(licenseCheckerCallback)
    }

Java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Call a wrapper method that initiates the license check
        doCheck();
        ...
    }
    ...
    private void doCheck() {
        checkLicenseButton.setEnabled(false);
        setProgressBarIndeterminateVisibility(true);
        statusText.setText(R.string.checking_license);
        checker.checkAccess(licenseCheckerCallback);
    }

ライセンス用公開鍵の埋め込み

Google Play サービスでは、アプリごとに、ライセンスとアプリ内課金に使用される 2,048 ビット RSA 公開鍵と秘密鍵のペアが自動的に生成されます。この鍵ペアは、アプリに一意に関連付けられています。アプリに関連付けられていても、この鍵ペアは、アプリに署名するために使用する(またはそこから抽出できる)鍵とは異なります

Google Play Console では、ライセンス用公開鍵については、Play Console にログインしているデベロッパーに公開されていますが、秘密鍵については、すべてのユーザーから見えない安全な場所に隠されています。あるアカウントで公開したアプリのライセンス チェックがリクエストされると、それに対するライセンス応答には、ライセンス サーバーによって署名がなされます。そのときに使用されるのが、そのアカウントのアプリに関連付けられた鍵ペアの秘密鍵です。LVL では、ライセンス応答を受信すると、そのアプリの公開鍵を使って応答の署名の検証が行われます。

アプリにライセンス機能を追加するには、そのアプリのライセンス用公開鍵を取得して、それをアプリ内にコピーする必要があります。ライセンス用公開鍵を取得する方法は次のとおりです。

  1. Google Play Console に移動してログインします。 必ずライセンス対象アプリの公開元アカウント(または公開予定アカウント)でログインしてください。
  2. アプリの詳細ページで [サービスと API] を見つけてクリックします。
  3. [サービスと API] ページで [ライセンスとアプリ内課金] セクションを見つけます。ライセンス用公開鍵は、[このアプリのライセンス用鍵] フィールドに記載されています。

アプリに公開鍵を追加するには、このフィールドから鍵の文字列をコピーして、文字列変数 BASE64_PUBLIC_KEY の値としてアプリに貼り付けます。コピーする際は、どの文字も除外せずに、鍵の文字列全体を選択してください。

LVL のサンプルアプリの該当部分を以下に示します。

Kotlin

private const val BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG ... " //truncated for this example
class LicensingActivity : AppCompatActivity() {
    ...
}

Java

public class MainActivity extends Activity {
    private static final String BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG ... "; //truncated for this example
    ...
}

LicenseChecker の onDestroy() メソッドの呼び出しによる IPC 接続の終了

最後に、アプリの Context が変化する前に LVL のクリーンアップを行うため、Activity の onDestroy() 実装に LicenseCheckeronDestroy() メソッドの呼び出しを追加します。この呼び出しを受けた LicenseChecker によって、Google Play アプリの ILicensingService に対するすべての IPC 接続が適切に終了され、ライセンス サービスとハンドラへのローカル参照がすべて削除されます。

LicenseCheckeronDestroy() メソッドを呼び出さなかった場合、アプリのライフサイクル全体にわたって問題が発生する可能性があります。たとえば、ライセンス チェックの進行中にユーザーが画面の向きを変更した場合、アプリの Context が破棄されます。その際にアプリで LicenseChecker の IPC 接続を適切に終了しないと、応答を受信した時点でアプリがクラッシュします。同様に、ライセンス チェックの進行中にユーザーがアプリを終了した場合、LicenseCheckeronDestroy() メソッドを適切に呼び出してライセンス サービスから切断しないと、応答を受信した時点でアプリがクラッシュします。

LVL に含まれるサンプルアプリの例を以下に示します。ここで、mCheckerLicenseChecker インスタンスです。

Kotlin

    override fun onDestroy() {
        super.onDestroy()
        checker.onDestroy()
        ...
    }

Java

    @Override
    protected void onDestroy() {
        super.onDestroy();
        checker.onDestroy();
        ...
    }

LicenseChecker を拡張または変更する場合は、LicenseCheckerfinishCheck() メソッドを呼び出して、すべての IPC 接続をクリーンアップする必要があります。

DeviceLimiter の実装

Policy を使って、1 つのライセンスを共用できるデバイスの数を制限した方がよい場合があります。そうすることで、ユーザーがライセンスを付与されたアプリを多数のデバイスに移動したり、移動したアプリを同じアカウント ID で使用したりするのを防ぐことができます。また、アプリの不正な共有(ライセンスを付与されたアカウントの情報を開示することで、他人が自ら所有するデバイスでそのアカウントを使ってログインし、アプリのライセンスを利用できるようにすること)も防ぐことができます。

LVL では、デバイスごとのライセンスをサポートするために、allowDeviceAccess() というメソッド 1 つを宣言する DeviceLimiter インターフェースを用意しています。LicenseValidator では、ライセンス サーバーからの応答を処理する際に、allowDeviceAccess() を呼び出して応答から抽出したユーザー ID 文字列を渡します。

デバイスの制限をサポートしない場合は、何もする必要はありませんLicenseChecker クラスでは、NullDeviceLimiter というデフォルトの実装が自動的に使用されます。名前が示すように、NullDeviceLimiter は「無処理」クラスであり、その allowDeviceAccess() メソッドからは、単純にすべてのユーザーとデバイスに対して LICENSED 応答が返されます。

注意: 次の理由により、ほとんどのアプリではデバイスごとのライセンスは推奨されません

  • ユーザーとデバイスのマッピングを管理するバックエンド サーバーを用意する必要がある。
  • ユーザーが別のデバイスで正規に購入したアプリが誤って使用拒否される可能性がある。

コードの難読化

アプリのセキュリティを確保するうえでは、特にライセンス機能、カスタムの制約や保護機能を使用した有料アプリの場合、アプリケーション コードを難読化することが重要です。コードを適切に難読化することで、悪意のあるユーザーがアプリのバイトコードを逆コンパイルし、ライセンス チェックを削除するなどの改変を加えて再コンパイルするのが困難になります。

Android アプリでは、数種類の難読化プログラムを利用できます。その中には、コード最適化機能も備えた ProGuard も含まれています。Google Play ライセンスを使用するすべてのアプリでは、ProGuard または同様のプログラムを使用してコードを難読化することを強くおすすめします

ライセンスを実装したアプリの公開

ライセンス機能の実装のテストが完了したら、Google Play でアプリを公開します。通常の手順に沿って、リリースの準備署名を行い、アプリを公開します。

サポートの受け方

アプリの実装、デプロイ、公開において疑問や問題が生じた場合は、下表のサポート リソースを使用してください。適切なフォーラムで質問をすることにより、必要なサポートを迅速に受けることができます。

表 2. Google Play ライセンス サービスのデベロッパー サポート リソース

サポートのタイプ リソース 取り扱い対象
開発とテストの問題 Google グループ: android-developers LVL のダウンロードと統合、ライブラリ プロジェクト、Policy に関する質問、ユーザー エクスペリエンスのアイデア、応答の処理、Obfuscator、IPC、テスト環境のセットアップ
Stack Overflow: http://stackoverflow.com/questions/tagged/android
アカウント、公開、デプロイに関する問題 Google Play ヘルプ フォーラム パブリッシャー アカウント、ライセンス用鍵ペア、テスト アカウント、サーバー応答、テスト応答、アプリのデプロイと結果
Google Play ライセンス サービスに関するよくある質問
LVL 公開バグトラッカー Marketlicensing プロジェクト公開バグトラッカー LVL ソースコードのクラスとインターフェースの実装に関連したバグや問題のレポート

上記のグループへの投稿方法に関する全般的な説明については、デベロッパー サポート リソースのコミュニティ リソースをご覧ください。

参考情報

LVL に含まれるサンプルアプリには、MainActivity クラスでライセンス チェックを開始してから結果を処理するまでの完全な例が示されています。