ブロック ストア

多くのユーザーは、新しい 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()
  }
}

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 でトークンを保存すると、トークンは暗号化され、デバイスにローカルに保存されます。

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

JavaKotlin
  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));
  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 を使用します。

JavaKotlin
  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));
  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 サービスはまずユーザーを検証してから、ブロックストア データを取得します。ユーザーは復元フローの一部としてアプリデータの復元にすでに同意しているため、追加の同意は必要ありません。ユーザーがアプリを開いたときに、retrieveBytes() を呼び出して Block Store からトークンをリクエストできます。取得したトークンを使用して、新しいデバイスでユーザーのログインを維持できます。

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

JavaKotlin
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<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));
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 に保存されているすべてのトークンを取得する方法の例です。

JavaKotlin
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));
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)
  }

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

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

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

トークンの削除

BlockStore からトークンを削除する必要がある理由は次のとおりです。

  • ユーザーがログアウト ユーザーフローを完了します。
  • トークンが失効しているか、無効です。

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

次の例は、特定のキーを削除する方法を示しています。

JavaKotlin
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)
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 に保存されているすべてのトークンを削除する方法を示しています。

JavaKotlin
// 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));

  val deleteAllRequest = DeleteBytesRequest.Builder()
  .setDeleteAll(true)
  .build()
retrieve bytes, the key BlockstoreClient.DEFAULT_BYTES_DATA_KEY can be used
in the RetrieveBytesRequest instance in order to get your saved data

The following example shows how to retrieve the default key.

Java

End-to-end encryption

In order for end-to-end encryption to be made available, the device must be running Android 9 or higher, and the user must have set a screen lock (PIN, pattern, or password) for their device. You can verify if encryption will be available on the device by calling isEndToEndEncryptionAvailable().

The following sample shows how to verify if encryption will be available during cloud backup:

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

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

クラウド バックアップを有効にするには、StoreBytesData オブジェクトに setShouldBackupToCloud() メソッドを追加します。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 を呼び出し、shouldBackUpToCloudtrue に設定してデータを保存します。
  3. O 以降のデバイスでは、Block Store クラウド バックアップを手動でトリガーできます。[設定] > [Google] > [バックアップ] に移動し、[今すぐバックアップ] ボタンをクリックします。
    1. Block Store クラウド バックアップが成功したことを確認するには、次の操作を行います。
      1. バックアップが完了したら、タグ「CloudSyncBpTkSvc」を含むログ行を検索します。
      2. 次のような行が表示されます。......, CloudSyncBpTkSvc: sync result: SUCCESS, ..., uploaded size: XXX bytes ...
    2. Block Store クラウド バックアップの後には、5 分間の「クールダウン」期間があります。この 5 分以内に [今すぐバックアップ] ボタンをクリックしても、別の Block Store クラウド バックアップはトリガーされません。
  4. 対象のデバイスを出荷時の設定にリセットし、クラウド復元フローを実施します。復元フロー中にテストアプリを復元するように選択します。クラウド復元フローの詳細については、サポートされているクラウド復元フローをご覧ください。
  5. 対象デバイスで、テストアプリを使用して Block store 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)以降を搭載している必要があります。