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 Project でサービス アカウントを作成する

このステップでは、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値はシークレットマネージャー内のシークレット名を参照します。この名前は先頭にhsdp-3p-キー- Play が提供する価値sdk_id。例えば、sdk_idABC秘密の名前はhsdp-3p-キー-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 3P SDK の暗号化された識別子。
左足 内部識別子。
3pAuth 発信者 ID SDK 識別子。
掲載情報 ストアのカスタム掲載情報の対象を指定するためのオプション パラメータ。 ×
リファラー オプションのリファラー トラッキング文字列。 ×

Python の例

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

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 の例

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

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_nameflag は対象アプリのドキュメント 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 の生成

以下は、私たちが作成し、内部で検証した C++の例です。enifd

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 ドキュメントにあるサンプルを基に作成されています。