ブロック ストア

新しい 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.2.0'
}

仕組み

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

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

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

トークンの保存

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

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

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() を呼び出してブロックストアにトークンをリクエストできます。取得したトークンは、ユーザーのログイン状態を新しいデバイスで維持するために使用できます。

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

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")
        }

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

クラウド バックアップを有効にするには、setShouldBackupToCloud() メソッドを StoreBytesData オブジェクトに追加します。Block Store は、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] > [バックアップ] で確認できます)、ブロックストアのデータはアプリのアンインストールや再インストール後も保持されます。

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

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

デバイス間

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

クラウド復元

  1. Blockstore API をテストアプリに統合します。テストアプリは Google Play ストアに送信する必要があります。
  2. ソースデバイスで、テストアプリを使用して Blockstore API を呼び出し、データを保存します。 shouldBackUpToCloud を true に設定します。
  3. 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)以降を搭載しているデバイスが復元機能を持つことを対象とします。

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

Cloud のバックアップと復元のフロー

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

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

対象デバイスは、ベンダーに基づいてサポートされます。Google Pixel デバイスでは、Android 9(API 29)からこの機能を使用できます。他のすべてのデバイスは、Android 12(API 31)以降を搭載している必要があります。