ブロック ストア

多くのユーザーは、新しい Android デバイスを設定するときに、依然として自分の認証情報を管理します。この手動プロセスは難しくなり、ユーザー エクスペリエンスが低下することがよくあります。Block Store API は、Google Play 開発者サービスを活用したライブラリであり、ユーザー パスワードの保存に伴う複雑さやセキュリティ上のリスクなしに、ユーザーの認証情報を保存する方法を提供することで、この問題を解決しようとしています。

Block Store API を使用すると、アプリはデータを保存し、後で新しいデバイスでユーザーの再認証を行うために取得できるようになります。これにより、新しいデバイスで初めてアプリを起動するときにログイン画面を見る必要がなくなるため、よりシームレスなエクスペリエンスをユーザーに提供できます。

Block Store を使用するメリットは次のとおりです。

  • デベロッパー向けの暗号化認証情報ストレージ ソリューション。認証情報は、可能な限りエンドツーエンドで暗号化されます。
  • ユーザー名とパスワードの代わりにトークンを保存します。
  • ログインフローの煩わしさを解消します。
  • 複雑なパスワードを管理する負担からユーザーを解放します。
  • Google がユーザーの ID を確認します。

始める前に

アプリを準備するには、以下のセクションに示す手順を完了します。

アプリを設定する

プロジェクト レベルの build.gradle ファイルで、buildscript セクションと allprojects セクションの両方に Google の Maven リポジトリを含めます。

buildscript {
  repositories {
    google()
    mavenCentral()
  }
}

allprojects {
  repositories {
    google()
    mavenCentral()
  }
}

モジュールの Gradle ビルドファイル(通常は app/build.gradle)に Block Store API 用の Google Play 開発者サービスの依存関係を追加します。

dependencies {
  implementation 'com.google.android.gms:play-services-auth-blockstore:16.2.0'
}

仕組み

ブロックストアを使用すると、デベロッパーは最大 16 バイトの配列の保存と復元ができます。 これにより、現在のユーザー セッションに関する重要な情報を保存できます。また、この情報を自由に保存できます。このデータはエンドツーエンドの暗号化が可能で、Block Store をサポートするインフラストラクチャはバックアップと復元のインフラストラクチャ上に構築されています。

このガイドでは、ユーザーのトークンを Block Store に保存するユースケースについて説明します。次の手順では、Block Store を利用するアプリがどのように動作するかを概説します。

  1. アプリの認証フローの最中またはその後で、ユーザーの認証トークンを後で取得できるように Block Store に保存できます。
  2. トークンはローカルに保存されます。また、可能な場合はクラウドにエンドツーエンドで暗号化することもできます。
  3. ユーザーが新しいデバイスで復元フローを開始すると、データが転送されます。
  4. 復元フロー中にユーザーがアプリを復元した場合、アプリは新しいデバイスの Block Store から保存したトークンを取得できます。

トークンを保存する

ユーザーがアプリにログインしたら、そのユーザー用に生成した認証トークンを Block Store に保存できます。エントリあたり最大 4 KB の一意の鍵ペア値を使用して、このトークンを格納できます。トークンを保存するには、StoreBytesData.Builder のインスタンスで setBytes()setKey() を呼び出して、ユーザーの認証情報をソースデバイスに保存します。Block Store でトークンを保存すると、トークンは暗号化され、デバイスにローカルに保存されます。

次のサンプルは、認証トークンをローカル デバイスに保存する方法を示しています。

Java

  BlockstoreClient client = Blockstore.getClient(this);
  byte[] bytes1 = new byte[] { 1, 2, 3, 4 };  // Store one data block.
  String key1 = "com.example.app.key1";
  StoreBytesData storeRequest1 = StoreBytesData.Builder()
          .setBytes(bytes1)
          // Call this method to set the key value pair the data should be associated with.
          .setKeys(Arrays.asList(key1))
          .build();
  client.storeBytes(storeRequest1)
    .addOnSuccessListener(result -> Log.d(TAG, "stored " + result + " bytes"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));

Kotlin

  val client = Blockstore.getClient(this)

  val bytes1 = byteArrayOf(1, 2, 3, 4) // Store one data block.
  val key1 = "com.example.app.key1"
  val storeRequest1 = StoreBytesData.Builder()
    .setBytes(bytes1) // Call this method to set the key value with which the data should be associated with.
    .setKeys(Arrays.asList(key1))
    .build()
  client.storeBytes(storeRequest1)
    .addOnSuccessListener { result: Int ->
      Log.d(TAG,
            "Stored $result bytes")
    }
    .addOnFailureListener { e ->
      Log.e(TAG, "Failed to store bytes", e)
    }

デフォルトのトークンを使用する

キーなしで StoreBytes を使用して保存されたデータには、デフォルトのキー BlockstoreClient.DEFAULT_BYTES_DATA_KEY が使用されます。

Java

  BlockstoreClient client = Blockstore.getClient(this);
  // The default key BlockstoreClient.DEFAULT_BYTES_DATA_KEY.
  byte[] bytes = new byte[] { 9, 10 };
  StoreBytesData storeRequest = StoreBytesData.Builder()
          .setBytes(bytes)
          .build();
  client.storeBytes(storeRequest)
    .addOnSuccessListener(result -> Log.d(TAG, "stored " + result + " bytes"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));

Kotlin

  val client = Blockstore.getClient(this);
  // the default key BlockstoreClient.DEFAULT_BYTES_DATA_KEY.
  val bytes = byteArrayOf(1, 2, 3, 4)
  val storeRequest = StoreBytesData.Builder()
    .setBytes(bytes)
    .build();
  client.storeBytes(storeRequest)
    .addOnSuccessListener { result: Int ->
      Log.d(TAG,
            "stored $result bytes")
    }
    .addOnFailureListener { e ->
      Log.e(TAG, "Failed to store bytes", e)
    }

トークンの取得

その後、ユーザーが新しいデバイスで復元フローを実行すると、Google Play 開発者サービスはまずユーザーを確認してから、Block Store のデータを取得します。ユーザーは復元フローの一環としてアプリデータの復元にすでに同意しているため、追加の同意は必要ありません。ユーザーがアプリを開いたら、retrieveBytes() を呼び出して Block Store にトークンをリクエストできます。取得したトークンを使用して、新しいデバイスでのユーザーのログイン状態を維持できます。

次のサンプルは、特定のキーに基づいて複数のトークンを取得する方法を示しています。

Java

BlockstoreClient client = Blockstore.getClient(this);

// Retrieve data associated with certain keys.
String key1 = "com.example.app.key1";
String key2 = "com.example.app.key2";
String key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY; // Used to retrieve data stored without a key

List requestedKeys = Arrays.asList(key1, key2, key3); // Add keys to array
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
    .setKeys(requestedKeys)
    .build();

client.retrieveBytes(retrieveRequest)
    .addOnSuccessListener(
        result -> {
          Map blockstoreDataMap = result.getBlockstoreDataMap();
          for (Map.Entry entry : blockstoreDataMap.entrySet()) {
            Log.d(TAG, String.format(
                "Retrieved bytes %s associated with key %s.",
                new String(entry.getValue().getBytes()), entry.getKey()));
          }
        })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));

Kotlin

val client = Blockstore.getClient(this)

// Retrieve data associated with certain keys.
val key1 = "com.example.app.key1"
val key2 = "com.example.app.key2"
val key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY // Used to retrieve data stored without a key

val requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array

val retrieveRequest = RetrieveBytesRequest.Builder()
  .setKeys(requestedKeys)
  .build()

client.retrieveBytes(retrieveRequest)
  .addOnSuccessListener { result: RetrieveBytesResponse ->
    val blockstoreDataMap =
      result.blockstoreDataMap
    for ((key, value) in blockstoreDataMap) {
      Log.d(ContentValues.TAG, String.format(
        "Retrieved bytes %s associated with key %s.",
        String(value.bytes), key))
    }
  }
  .addOnFailureListener { e: Exception? ->
    Log.e(ContentValues.TAG,
          "Failed to store bytes",
          e)
  }

すべてのトークンを取得しています。

以下は、BlockStore に保存されたすべてのトークンを取得する方法の例です。

Java

BlockstoreClient client = Blockstore.getClient(this)

// Retrieve all data.
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
    .setRetrieveAll(true)
    .build();

client.retrieveBytes(retrieveRequest)
    .addOnSuccessListener(
        result -> {
          Map blockstoreDataMap = result.getBlockstoreDataMap();
          for (Map.Entry entry : blockstoreDataMap.entrySet()) {
            Log.d(TAG, String.format(
                "Retrieved bytes %s associated with key %s.",
                new String(entry.getValue().getBytes()), entry.getKey()));
          }
        })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));

Kotlin

val client = Blockstore.getClient(this)

val retrieveRequest = RetrieveBytesRequest.Builder()
  .setRetrieveAll(true)
  .build()

client.retrieveBytes(retrieveRequest)
  .addOnSuccessListener { result: RetrieveBytesResponse ->
    val blockstoreDataMap =
      result.blockstoreDataMap
    for ((key, value) in blockstoreDataMap) {
      Log.d(ContentValues.TAG, String.format(
        "Retrieved bytes %s associated with key %s.",
        String(value.bytes), key))
    }
  }
  .addOnFailureListener { e: Exception? ->
    Log.e(ContentValues.TAG,
          "Failed to store bytes",
          e)
  }

以下に、デフォルトの鍵を取得する方法の例を示します。

Java

BlockStoreClient client = Blockstore.getClient(this);
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
    .setKeys(Arrays.asList(BlockstoreClient.DEFAULT_BYTES_DATA_KEY))
    .build();
client.retrieveBytes(retrieveRequest);

Kotlin

val client = Blockstore.getClient(this)

val retrieveRequest = RetrieveBytesRequest.Builder()
  .setKeys(Arrays.asList(BlockstoreClient.DEFAULT_BYTES_DATA_KEY))
  .build()
client.retrieveBytes(retrieveRequest)

トークンの削除

次の理由により、BlockStore からのトークンの削除が必要になる場合があります。

  • ユーザーがログアウト ユーザーフローを実行します。
  • トークンが取り消されているか、無効です。

トークンを取得する場合と同様に、削除が必要なキーの配列を設定して、削除が必要なトークンを指定できます。

以下は、特定のキーを削除する例です。

Java

BlockstoreClient client = Blockstore.getClient(this);

// Delete data associated with certain keys.
String key1 = "com.example.app.key1";
String key2 = "com.example.app.key2";
String key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY; // Used to delete data stored without key

List requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array
DeleteBytesRequest deleteRequest = new DeleteBytesRequest.Builder()
      .setKeys(requestedKeys)
      .build();
client.deleteBytes(deleteRequest)

Kotlin

val client = Blockstore.getClient(this)

// Retrieve data associated with certain keys.
val key1 = "com.example.app.key1"
val key2 = "com.example.app.key2"
val key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY // Used to retrieve data stored without a key

val requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array

val retrieveRequest = DeleteBytesRequest.Builder()
      .setKeys(requestedKeys)
      .build()

client.deleteBytes(retrieveRequest)

すべてのトークンを削除

次の例では、現在 BlockStore に保存されているすべてのトークンを削除します。

Java

// Delete all data.
DeleteBytesRequest deleteAllRequest = new DeleteBytesRequest.Builder()
      .setDeleteAll(true)
      .build();
client.deleteBytes(deleteAllRequest)
.addOnSuccessListener(result -> Log.d(TAG, "Any data found and deleted? " + result));

Kotlin

  val deleteAllRequest = DeleteBytesRequest.Builder()
  .setDeleteAll(true)
  .build()
client.deleteBytes(deleteAllRequest)
  .addOnSuccessListener { result: Boolean ->
    Log.d(TAG,
          "Any data found and deleted? $result")
  }

エンドツーエンドの暗号化

エンドツーエンドの暗号化を使用するには、デバイスに Android 9 以降が搭載されており、ユーザーがデバイスに画面ロック(PIN、パターン、またはパスワード)を設定している必要があります。デバイスで暗号化が利用可能かどうかを確認するには、isEndToEndEncryptionAvailable() を呼び出します。

次のサンプルは、クラウド バックアップ中に暗号化が使用可能かどうかを確認する方法を示しています。

client.isEndToEndEncryptionAvailable()
        .addOnSuccessListener { result ->
          Log.d(TAG, "Will Block Store cloud backup be end-to-end encrypted? $result")
        }

クラウド バックアップを有効にする

クラウド バックアップを有効にするには、StoreBytesData オブジェクトに setShouldBackupToCloud() メソッドを追加します。setShouldBackupToCloud() が true に設定されている場合、ブロックストアは保存されたバイト数を定期的にクラウドにバックアップします。

次のサンプルは、クラウド バックアップがエンドツーエンドで暗号化されている場合にのみ、クラウド バックアップを有効にする方法を示しています。

val client = Blockstore.getClient(this)
val storeBytesDataBuilder = StoreBytesData.Builder()
        .setBytes(/* BYTE_ARRAY */)

client.isEndToEndEncryptionAvailable()
        .addOnSuccessListener { isE2EEAvailable ->
          if (isE2EEAvailable) {
            storeBytesDataBuilder.setShouldBackupToCloud(true)
            Log.d(TAG, "E2EE is available, enable backing up bytes to the cloud.")

            client.storeBytes(storeBytesDataBuilder.build())
                .addOnSuccessListener { result ->
                  Log.d(TAG, "stored: ${result.getBytesStored()}")
                }.addOnFailureListener { e ->
                  Log.e(TAG, “Failed to store bytes”, e)
                }
          } else {
            Log.d(TAG, "E2EE is not available, only store bytes for D2D restore.")
          }
        }

テスト方法

復元フローをテストするには、開発中に次の方法を使用します。

同じデバイスのアンインストールと再インストール

ユーザーがバックアップ サービスを有効にしている場合([設定] > [Google] > [バックアップ] で確認できます)、Block Store のデータはアプリのアンインストールや再インストール後も保持されます。

テストの手順は次のとおりです。

  1. BlockStore API をテストアプリに統合します。
  2. テストアプリを使用して BlockStore API を呼び出し、データを保存します。
  3. テストアプリをアンインストールしてから、同じデバイスにアプリを再インストールします。
  4. テストアプリを使用して BlockStore API を呼び出し、データを取得します。
  5. 取得したバイト数がアンインストール前に保存されていたバイト数と同じであることを確認します。

デバイス間

ほとんどの場合、これを行うには対象デバイスを出荷時の設定にリセットする必要があります。その後、Android のワイヤレス復元フローまたは Google ケーブル復元(サポートされているデバイスの場合)を開始できます。

クラウドによる復元

  1. Blockstore API をテストアプリに統合します。テストアプリは Google Play ストアに送信する必要があります。
  2. ソースデバイスで、 shouldBackUpToCloud を true に設定して、テストアプリを使用して Blockstore API を呼び出してデータを保存します。
  3. Android O 以降のデバイスでは、Block Store のクラウド バックアップを手動でトリガーできます。[設定] > [Google] > [バックアップ] に移動し、[今すぐバックアップ] ボタンをクリックします。
    1. Block Store のクラウド バックアップが成功したことを確認するには、次の操作を行います。
      1. バックアップが完了したら、「CloudSyncBpTkSvc」タグの付いたログ行を検索します。
      2. 「......, CloudSyncBpTkSvc: sync result: SUCCESS, ..., upload size: XXX bytes ...」のような行が表示されます。
    2. Block Store のクラウド バックアップ後、5 分間の「クールダウン」期間があります。 その 5 分以内に [今すぐバックアップ] ボタンをクリックしても、別の Block Store クラウド バックアップはトリガーされません。
  4. 対象デバイスを出荷時の設定にリセットし、クラウド復元フローを行います。復元フロー中にテストアプリを復元する場合に選択します。クラウド復元フローの詳細については、サポートされているクラウド復元フローをご覧ください。
  5. ターゲット デバイスで、テストアプリを使用して Blockstore API を呼び出し、データを取得します。
  6. 取得したバイトがソースデバイスに保存されていたバイト数と同じであることを確認します。

デバイスの要件

エンドツーエンドの暗号化

  • エンドツーエンドの暗号化は、Android 9(API 29)以降を搭載したデバイスでサポートされています。
  • エンドツーエンドの暗号化が有効になり、ユーザーのデータを正しく暗号化するには、PIN、パターン、またはパスワードで画面ロックが設定されている必要があります。

デバイス間の復元フロー

デバイス間の復元には、ソースデバイスとターゲット デバイスが必要です。データを転送する 2 つのデバイスになります。

バックアップするには、ソースデバイスが Android 6(API 23)以降である必要があります。

Android 9(API 29)以降を搭載しているデバイスをターゲットにし、復元できるようにします。

デバイス間の復元フローについて詳しくは、こちらをご覧ください。

クラウドのバックアップと復元のフロー

クラウドのバックアップと復元には、ソースデバイスとターゲット デバイスが必要です。

バックアップするには、ソースデバイスが Android 6(API 23)以降である必要があります。

対象デバイスはベンダーによって異なります。Google Pixel デバイスは Android 9(API 29)からこの機能を使用できます。その他のすべてのデバイスは Android 12(API 31)以降を搭載している必要があります。