Google Play インラインインストール(SDK)

このページでは、サードパーティ SDK が インライン インストールを統合する方法について説明します。インライン インストールは、Google Play の新しいテスト機能で、Google Play アプリの商品詳細をハーフシート インターフェースで表示します。インライン インストールにより、ユーザーはアプリのコンテキストを離れることなく、シームレスなアプリ インストール フローを体験できます。

サードパーティの SDK デベロッパーは、インライン インストール機能を SDK に統合して、それらの SDK を使用するアプリ デベロッパーがアプリのインライン インストールにアクセスできるようにすることができます。

要件

アプリにインライン インストール ハーフシート インターフェースを表示するには:

  • Google Play の最小バージョンは 40.4 である必要があります。
  • Android API レベルは 23 以上である必要があります。

プロセスのアーキテクチャ

インライン インストール プロセスのアーキテクチャを次の図に示します。

図 1: インライン インストール プロセス アーキテクチャの概要。
  1. Google Play サーバーは、関連データ(AEAD)暗号鍵による認証済み暗号化を生成し、その鍵を Google Cloud Platform(GCP)Secret Manager インスタンスに取り込みます。
  2. サードパーティのインテグレータは、GCP Secret Manager から AEAD 鍵を取得します。
  3. サードパーティのインテグレータは、インライン インストールの Intent データを暗号化し、インライン インストール インテントの呼び出しに使用されるディープリンクで渡される暗号文を生成し、レスポンスでクライアントにディープリンクを送信します。
  4. ディープリンクがたどられると、Google Play アプリがインテントを処理します。

インライン インストール プロセスを使用するようにサードパーティ SDK を構成するには、次の手順に沿って操作します。

Google Cloud プロジェクトでサービス アカウントを作成する

このステップでは、Google Cloud コンソールを使用してサービス アカウントを設定します。

  1. Google Cloud プロジェクトを設定する:
    • Google Cloud 組織を作成します。Google Workspace アカウントまたは Cloud Identity アカウントを作成してドメイン名に関連付けると、組織リソースが自動的に作成されます。詳細については、組織リソースの作成と管理をご覧ください。
    • 前の手順で作成した Google Cloud アカウントを使用して GCP Console にログインし、Google Cloud プロジェクトを作成します。詳細については、Google Cloud プロジェクトを作成するをご覧ください。
  2. 作成した Google Cloud プロジェクトにサービス アカウントを作成します。サービス アカウントは、サーバーに代わって対称鍵にアクセスするための Google Cloud Identity として使用されます。詳細については、サービス アカウントを作成するをご覧ください。
  3. 関心登録フォームに入力した Google Workspace お客様 ID(GWCID)/ Dasher ID を使用します。
  4. そのサービス アカウントの秘密鍵を作成してダウンロードします。
  5. そのサービス アカウントのキーを作成します。詳細については、サービス アカウント キーを作成するをご覧ください。
  6. サービス アカウント キーをダウンロードし、サーバーでアクセスできるようにします。これは、対称鍵の Google Cloud リソースにアクセスするための認証に使用されます。詳細については、サービス アカウント キーを取得するをご覧ください。

認証情報を取得する

このステップでは、Secret Manager から対称鍵を取得し、独自のサーバー ストレージに安全に保存します(JSON ファイルなど)。この鍵は、インライン インストール データの暗号テキストを生成するために使用されます。

secret_id/secretId の値は、Secret Manager 内の Secret 名を参照します。この名前は、Play が提供する値 sdk_id の前に hsdp-3p-key- を付加することで生成されます。たとえば、sdk_idabc の場合、シークレット名は hsdp-3p-key-abc です。

シークレット バージョンは、毎週火曜日の午後 2 時(UTC)に更新されます。2 番目に新しい鍵は次のローテーションまで引き続き機能し、鍵マテリアルは毎週新たに取得して保存する必要があります。

Python の例

次のコード例では、JSON ファイルに保存されているアクセス トークンを使用して GCP Secret Manager の鍵マテリアルにアクセスし、コンソールに出力します。

#!/usr/bin/env python3
# Import the Secret Manager client library.
from google.cloud import secretmanager
from google.oauth2 import service_account
import google_crc32c

# Create a service account key file.
service_account_key_file = "<json key file of the service account>"
credentials = service_account.Credentials.from_service_account_file(service_account_key_file)

# Create the Secret Manager client.
client = secretmanager.SecretManagerServiceClient(
  credentials=credentials
)

# Build the resource name of the secret version.
name = f"projects/prod-play-hsdp-3p-caller-auth/secrets/<secret_id>/versions/latest"

# Access the secret version.
response = client.access_secret_version(request={"name": name})

# Verify payload checksum.
crc32c = google_crc32c.Checksum()
crc32c.update(response.payload.data)
if response.payload.data_crc32c != int(crc32c.hexdigest(), 16):
    print("Data corruption detected.")

# A keyset created with "tinkey create-keyset --key-template=AES256_GCM". Note
# that this keyset has the secret key information in cleartext.
keyset = response.payload.data.decode("UTF-8")

# WARNING: Do not print the secret in a production environment. Please store it
# in a secure storage.
with open('<key file name>', 'w') as f:
    f.write(keyset)

Java の例

次のコード例では、JSON ファイルに保存されているアクセス トークンを使用して、GCP Secret Manager のキーマテリアルにアクセスし、JSON ファイルに書き込みます。

import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
import com.google.cloud.secretmanager.v1.SecretVersionName;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.zip.CRC32C;
import java.util.zip.Checksum;

/** */
final class ThirdPartySecretAccessGuide {

  private ThirdPartySecretAccessGuide() {}

  public static void main(String[] args) throws IOException {
    accessSecretVersion();
  }

  public static void accessSecretVersion() throws IOException {
    // TODO(developer): Replace these variables before running the sample.
    String projectId = "projectId";
    String secretId = "secretId";
    String versionId = "versionId";
    String accessTokenPrivateKeyPath = "path/to/credentials.json";
    String secretMaterialOutputPath = "path/to/secret.json";
    accessSecretVersion(
        projectId, secretId, versionId, accessTokenPrivateKeyPath, secretMaterialOutputPath);
  }

  // Access the payload for the given secret version if one exists. The version
  // can be a version number as a string (e.g. "5") or an alias (e.g. "latest").
  public static void accessSecretVersion(
      String projectId,
      String secretId,
      String versionId,
      String accessTokenPrivateKeyPath,
      String secretMaterialOutputPath)
      throws IOException {

    // We can explicitly instantiate the SecretManagerServiceClient (below) from a json file if we:
    // 1. Create a CredentialsProvider from a FileInputStream of the JSON file,
    CredentialsProvider credentialsProvider =
        FixedCredentialsProvider.create(
            ServiceAccountCredentials.fromStream(new FileInputStream(accessTokenPrivateKeyPath)));

    // 2. Build a SecretManagerService Settings object from that credentials provider, and
    SecretManagerServiceSettings secretManagerServiceSettings =
        SecretManagerServiceSettings.newBuilder()
            .setCredentialsProvider(credentialsProvider)
            .build();

    // 3. Initialize client that will be used to send requests by passing the settings object to
    // create(). This client only needs to be created once, and can be reused for multiple requests.
    // After completing all of your requests, call the "close" method on the client to safely clean
    // up any remaining background resources.
    try (SecretManagerServiceClient client =
        SecretManagerServiceClient.create(secretManagerServiceSettings)) {
      SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, versionId);

      // Access the secret version.
      AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);

      // Verify checksum. The used library is available in Java 9+.
      // If using Java 8, you may use the following:
      // https://github.com/google/guava/blob/e62d6a0456420d295089a9c319b7593a3eae4a83/guava/src/com/google/common/hash/Hashing.java#L395
      byte[] data = response.getPayload().getData().toByteArray();
      Checksum checksum = new CRC32C();
      checksum.update(data, 0, data.length);
      if (response.getPayload().getDataCrc32C() != checksum.getValue()) {
        System.out.printf("Data corruption detected.");
        return;
      }

      String payload = response.getPayload().getData().toStringUtf8();
      // Print the secret payload.
      //
      // WARNING: Do not print the secret in a production environment - this
      // snippet is showing how to access the secret material.
      System.out.printf("Plaintext: %s\n", payload);

      // Write the JSON secret material payload to a json file
      try (PrintWriter out =
          new PrintWriter(Files.newBufferedWriter(Paths.get(secretMaterialOutputPath), UTF_8))) {
        out.write(payload);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

アプリケーションのデフォルト認証情報を設定する

Java 実装で CredentialsProvider を使用して秘密鍵を JSON ファイルに渡さない場合は、アプリケーションのデフォルト認証情報(ADC)を設定して実装を変更できます。

  1. クライアント ライブラリにサービス アカウント キーの場所を伝えます。
  2. Java プロジェクトに Maven 依存関係を追加します。
  3. SecretManagerServiceClient.create() を呼び出します。これにより、認証が自動的に取得されます(ステップ 1 のため)。

次の手順では、Java 実装を次のように変更します。

  • CredentialsProvider オブジェクトと SecretManagerServiceSettings オブジェクトを作成する必要がなくなります。
  • 引数を含まないように SecretManagerServiceClient.create() の呼び出しを変更します。

暗号文を作成してディープリンクを生成する

このステップでは、Tink 暗号ライブラリを使用して、InlineInstallData protobuf オブジェクトから enifdInlineInstallData 暗号テキスト)を作成します。InlineInstallData proto は次のように定義されます。

syntax = "proto2";
package hsdpexperiments;
option java_package = "com.google.hsdpexperiments";
option java_multiple_files = true;

// InlineInstallData is used by 3p auth callers to generate "encrypted inline
// flow data" (enifd) which is decrypted in PGS to verify authenticity and
// freshness.
message InlineInstallData {
  // The timestamp which indicates the time encrypted data is generated.
  // Used to validate freshness (i.e. generation time in past 4 hours).
  // Required.
  optional int64 timestamp_ms = 1;

  // The docid of the app that we want to open inline install page for.
  // This is the package name.
  // Required.
  optional string target_package_name = 2;

  // This is the name of the app requesting the ad from Google Ad Serving
  // system.
  // Required.
  optional string caller_package_name = 3;

  // This is the advertising id that will be collected by 3P Ad SDKs.
  // Optional.
  optional string advertising_id = 4;

  // This is used to indicate the network from where the inline install was
  // requested.
  // Required.
  optional string ad_network_id = 5;
}

このステップでは、次のパラメータを使用してディープリンク URL も作成します。

フィールド 説明 必須
id インストールするアプリのパッケージ名 はい
インライン インライン インストールのハーフシートがリクエストされた場合は true に設定します。false の場合、インテントは Google Play にディープリンクします。 はい
enifd サードパーティ SDK の暗号化された識別子。 はい
lft 内部識別子。 はい
3pAuthCallerId SDK 識別子。 はい
掲載情報 ストアのカスタム掲載情報のターゲットを指定するオプションのパラメータ。 いいえ
referrer オプションのリファラー トラッキング文字列。 いいえ

Python の例

次のコマンドは、InlineInstallData.proto から Python コードを生成します。

protoc InlineInstallData.proto --python_out=.

次の Python サンプルコードは、InlineInstallData を構築し、対称鍵で暗号化して暗号文を作成します。

#!/usr/bin/env python3

# Import the Secret Manager client library.
import base64
import time
import inline_install_data_pb2 as InlineInstallData
import tink
from tink import aead
from tink import cleartext_keyset_handle

# Read the stored symmetric key.
with open("example3psecret.json", "r") as f:
  keyset = f.read()

"""Encrypt and decrypt using AEAD."""
# Register the AEAD key managers. This is needed to create an Aead primitive later.
aead.register()

# Create a keyset handle from the cleartext keyset in the previous
# step. The keyset handle provides abstract access to the underlying keyset to
# limit access of the raw key material. WARNING: In practice, it is unlikely
# you will want to use a cleartext_keyset_handle, as it implies that your key
# material is passed in cleartext, which is a security risk.
keyset_handle = cleartext_keyset_handle.read(tink.JsonKeysetReader(keyset))

# Retrieve the Aead primitive we want to use from the keyset handle.
primitive = keyset_handle.primitive(aead.Aead)

inlineInstallData = InlineInstallData.InlineInstallData()
inlineInstallData.timestamp_ms = int(time.time() * 1000)
inlineInstallData.target_package_name = "x.y.z"
inlineInstallData.caller_package_name = "a.b.c"
inlineInstallData.ad_network_id = "<sdk_id>"

# Use the primitive to encrypt a message. In this case the primary key of the
# keyset will be used (which is also the only key in this example).
ciphertext = primitive.encrypt(inlineInstallData.SerializeToString(), b'<sdk_id>')
print(f"InlineInstallData Ciphertext: {ciphertext}")

# Base64 Encoded InlineInstallData Ciphertext
enifd = base64.urlsafe_b64encode(ciphertext).decode('utf-8')
print(enifd)

# Deeplink
print(f"https://play.google.com/d?id={inlineInstallData.target_package_name}\&inline=true\&enifd={enifd}\&lft=1\&3pAuthCallerId={inlineInstallData.ad_network_id}")

次のコマンドを実行して Python スクリプトを実行します。

python <file_name>.py

Java の例

次のコマンドは、InlineInstallData.proto から Java コードを生成します。

protoc InlineInstallData.proto --java_out=.

次の Java サンプルコードは、InlineInstallData を構築し、対称鍵で暗号化して暗号文を作成します。

package com.google.hsdpexperiments;

import static com.google.common.io.BaseEncoding.base64Url;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.flags.Flag;
import com.google.common.flags.FlagSpec;
import com.google.common.flags.Flags;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.aead.AeadConfig;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Security;
import java.time.Duration;
import org.conscrypt.Conscrypt;

/** info on encryption in https://github.com/google/tink#learn-more */
final class ThirdPartyEnifdGuide {

  @FlagSpec(
      name = "third_party_id",
      help = "the identifier associated with the 3p for which to generate the enifd")
  private static final Flag<String> thirdPartyAuthCallerId = Flag.value("");

  @FlagSpec(name = "package_name", help = "the package name of the target app")
  private static final Flag<String> packageName = Flag.value("");


  @FlagSpec(name = "caller_package_name", help = "the package name of the caller app")
  private static final Flag<String> callerPackageName = Flag.value("");

  @FlagSpec(name = "secret_filename", help = "the path to the json file with the secret material")
  private static final Flag<String> secretFilename = Flag.value("");

  private ThirdPartyEnifdGuide() {}

  public static void main(String[] args) throws Exception {
    // parse flags
    Flags.parse(args);

    // File keyFile = new File(args[0]);
    Path keyFile = Paths.get(secretFilename.get());

    // Create structured inline flow data
    InlineInstallData idrp =
        InlineInstallData.newBuilder()
            .setTargetPackageName(packageName.get())
            .setCallerPackageName(callerPackageName.get())
            .setTimestampMs(System.currentTimeMillis())
            .setAdNetworkId(thirdPartyAuthCallerId.get())
            .build();

    // we can print this out here to make sure it's well formatted, this will help debug
    System.out.println(idrp.toString());

    // Register all AEAD key types with the Tink runtime.
    Conscrypt.checkAvailability();
    Security.addProvider(Conscrypt.newProvider());
    AeadConfig.register();

    // Read AEAD key downloaded from secretmanager into keysethandle
    KeysetHandle handle =
        TinkJsonProtoKeysetFormat.parseKeyset(
            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());

    // Generate enifd using tink library
    Aead aead = handle.getPrimitive(Aead.class);
    byte[] plaintext = idrp.toByteArray();
    byte[] ciphertext = aead.encrypt(plaintext, thirdPartyAuthCallerId.get().getBytes(UTF_8));
    String enifd = base64Url().omitPadding().encode(ciphertext);

    // Build deeplink, escaping ampersands (TODO: verify this is necessary while testing e2e)
    String deeplink =
        "https://play.google.com/d?id="
            + packageName.get()
            + "\\&inline=true\\&enifd="
            + enifd
            + "\\&lft=1\\&3pAuthCallerId="
            + thirdPartyAuthCallerId.get();

    System.out.println(deeplink);
  }
}

最後に、Java プログラムをバイナリにビルドし、次のコードを使用して呼び出します。

path/to/binary/ThirdPartyEnifdGuide --secret_filename=path/to/jsonfile/example3psecret.json --package_name=<package_name_of_target_app> --third_party_id=<3p_caller_auth_id>
  • secret_filename フラグは、シークレット マテリアルを含む JSON ファイルのパスを指定します。
  • package_name フラグは、ターゲット アプリのドキュメント ID です。
  • third_party_id フラグは、サードパーティの発信者認証 ID(つまり <sdk_id>)を指定するために使用されます。

インライン インストール インテントを起動する

前の手順で生成されたディープリンクをテストするには、Android デバイス(USB デバッグが有効になっていることを確認)を ADB がインストールされているワークステーションに接続し、次のコマンドを実行します。

adb shell am start "<output_from_the_previous_python_or_java_code>"

クライアント コードで、次のいずれかのメソッド(Kotlin または Java)を使用してインテントを送信します。

Kotlin

val intent = Intent(Intent.ACTION_VIEW)
val deepLinkUrl = "<output_from_the_previous_python_or_java_code>"
intent.setPackage("com.android.vending")
intent.data = Uri.parse(deepLinkUrl)
val packageManager = context.getPackageManager()
if (intent.resolveActivity(packageManager) != null) {
  startActivityForResult(intent, 0)
} else {
  // Fallback to deep linking to full Play Store.
}

Java

Intent intent = new Intent(Intent.ACTION_VIEW);
String id = "exampleAppToBeInstalledId";
String deepLinkUrl = "<output_from_the_previous_python_or_java_code>";
intent.setPackage("com.android.vending");
intent.setData(Uri.parse(deepLinkUrl));
PackageManager packageManager = context.getPackageManager();
if (intent.resolveActivity(packageManager) != null) {
  startActivityForResult(intent, 0);
} else {
  // Fallback to deep linking to full Play Store.
}

付録

以降のセクションでは、特定のユースケースに関する追加のガイダンスを提供します。

Python 環境を準備する

Python のサンプルコードを実行するには、ワークステーションに Python 環境を設定し、必要な依存関係をインストールします。

  1. Python 環境を設定します。

    1. python3.11 をインストールします(すでにインストールされている場合は、この手順をスキップします)。

      sudo apt install python3.11
      
    2. pip をインストールします。

      sudo apt-get install pip
      
    3. virtualenv をインストールします。

      sudo apt install python3-virtualenv
      
    4. 仮想環境を作成します(Tink 依存関係に必要)。

      virtualenv inlineinstall --python=/usr/bin/python3.11
      
  2. 仮想環境に入ります。

    source inlineinstall/bin/activate
    
  3. pip を更新します。

    python -m pip install --upgrade pip
    
  4. 必要な依存関係をインストールします。

    1. Tink をインストールします。

      pip install tink
      
    2. Google crc32c をインストールします。

      pip install google-crc32c
      
    3. Secret Manager をインストールします。

      pip install google-cloud-secret-manager
      
    4. protobuf コンパイラをインストールします。

      sudo apt install protobuf-compiler
      

C++ enifd の生成

以下は、enifd を生成するために内部で作成して検証した C++ の例です。

enifd の生成は、次のように C++ コードを使用して実行できます。

// A command-line example for using Tink AEAD w/ key template aes128gcmsiv to
// encrypt an InlineInstallData proto.
#include <chrono>
#include <iostream>
#include <memory>
#include <string>

#include "<path_to_protoc_output>/inline_install_data.proto.h"
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/strings/escaping.h"
#include "absl/strings/string_view.h"
#include "tink/cc/aead.h"
#include "tink/cc/aead_config.h"
#include "tink/cc/aead_key_templates.h"
#include "tink/cc/config/global_registry.h"
#include "tink/cc/examples/util/util.h"
#include "tink/cc/keyset_handle.h"
#include "tink/cc/util/status.h"
#include "tink/cc/util/statusor.h"

ABSL_FLAG(std::string, keyset_filename, "",
          "Keyset file (downloaded from secretmanager) in JSON format");
ABSL_FLAG(std::string, associated_data, "",
          "Associated data for AEAD (default: empty");

namespace {

using ::crypto::tink::Aead;
using ::crypto::tink::AeadConfig;
using ::crypto::tink::KeysetHandle;
using ::crypto::tink::util::Status;
using ::crypto::tink::util::StatusOr;

}  // namespace

namespace tink_cc_examples {

// AEAD example CLI implementation.
void AeadCli(const std::string& keyset_filename,
             absl::string_view associated_data) {
  Status result = AeadConfig::Register();
  if (!result.ok()) {
    std::clog << "Failed to register AeadConfig";
    return;
  }

  // Read the keyset from file.
  StatusOr<std::unique_ptr<KeysetHandle>> keyset_handle =
      ReadJsonCleartextKeyset(keyset_filename);
  if (!keyset_handle.ok()) {
    std::clog << "Failed to read json keyset";
    return;
  }

  // Get the primitive.
  StatusOr<std::unique_ptr<Aead>> aead =
      (*keyset_handle)
          ->GetPrimitive<crypto::tink::Aead>(
              crypto::tink::ConfigGlobalRegistry());
  if (!aead.ok()) {
    std::clog << "Failed to get primitive";
    return;
  }

  // Instantiate the enifd.
  hsdpexperiments::InlineInstallData iid;

  iid.set_timestamp_ms(std::chrono::duration_cast<std::chrono::milliseconds>(
                           std::chrono::system_clock::now().time_since_epoch())
                           .count());
  iid.set_target_package_name("<TARGET_PACKAGE_NAME>");
  iid.set_caller_package_name("<CALLER_PACKAGE_NAME>");
  iid.set_ad_network_id("<SDK_ID>");

  // Compute the output.
  StatusOr<std::string> encrypt_result =
      (*aead)->Encrypt(iid.SerializeAsString(), associated_data);
  if (!encrypt_result.ok()) {
    std::clog << "Failed to encrypt Inline Install Data";
    return;
  }
  const std::string& output = encrypt_result.value();

  std::string enifd;
  absl::WebSafeBase64Escape(output, &enifd);

  std::clog << "enifd: " << enifd << '\n';
}

}  // namespace tink_cc_examples

int main(int argc, char** argv) {
  absl::ParseCommandLine(argc, argv);

  std::string keyset_filename = absl::GetFlag(FLAGS_keyset_filename);
  std::string associated_data = absl::GetFlag(FLAGS_associated_data);

  std::clog << "Using keyset from file " << keyset_filename
            << " to AEAD-encrypt inline install data with associated data '"
            << associated_data << "'." << '\n';

  tink_cc_examples::AeadCli(keyset_filename, associated_data);
  return 0;
}

このコードは、Tink ドキュメントにあるサンプルから抜粋したものです。