ブロック ストア

多くのユーザーは、新しい Android 搭載デバイスをセットアップする際に、依然として独自の認証情報を管理しています。この手動プロセスは困難になることがあり、多くの場合、ユーザー エクスペリエンスの低下につながります。Google Play 開発者サービスによるライブラリである Block Store API は、ユーザー パスワードの保存に伴う複雑さやセキュリティ リスクを排除して、アプリがユーザーの認証情報を保存できるようにすることで、この問題を解決します。

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

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

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

始める前に

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

アプリを設定する

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

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

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

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

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

仕組み

Block Store を使用すると、デベロッパーは最大 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<String> requestedKeys = Arrays.asList(key1, key2, key3); // Add keys to array
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
    .setKeys(requestedKeys)
    .build();

client.retrieveBytes(retrieveRequest)
    .addOnSuccessListener(
        result -> {
          Map<String, BlockstoreData> blockstoreDataMap = result.getBlockstoreDataMap();
          for (Map.Entry<String, BlockstoreData> 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<String, BlockstoreData> blockstoreDataMap = result.getBlockstoreDataMap();
          for (Map.Entry<String, BlockstoreData> 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<String> 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)

すべてのトークンを削除

次の例は、Block Store に保存されているすべてのトークンを削除する方法を示しています。

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

// Delete all data.
val deleteAllRequest = DeleteBytesRequest.Builder()
        .setDeleteAll(true)
        .build()
client.deleteBytes(deleteAllRequest)
  .addOnSuccessListener { result -> 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")
        }

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

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

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

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

デバイス間

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

クラウドによる復元

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

デバイスの要件

Block Store オペレーションに参加するデバイスの次の要件を確認してください。

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

  • エンドツーエンドの暗号化は、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)以降を搭載している必要があります。