本页介绍了第三方 SDK 如何集成内嵌安装,这是 Google Play 的一项新测试功能,可在半屏界面中显示 Google Play 应用产品详情。借助内嵌安装,用户无需离开应用上下文即可体验顺畅的应用安装流程。
第三方 SDK 开发者可以将内嵌安装功能集成到其 SDK 中,以便使用这些 SDK 的应用开发者能够为其应用启用内嵌安装。
要求
如需在应用中显示内嵌安装半屏界面,请执行以下操作:
- 最低 Google Play 版本必须为 40.4。
- Android API 级别必须为 23 或更高级别。
流程架构
内嵌安装流程架构如下图所示:
- Google Play 服务器会生成经过身份验证的关联数据加密 (AEAD) 加密密钥,并将这些密钥提取到 Google Cloud Platform (GCP) Secret Manager 实例中。
- 第三方集成商从 GCP Secret Manager 检索 AEAD 密钥。
- 第三方集成商会对内嵌安装
Intent数据进行加密,生成在用于调用内嵌安装 intent 的深层链接中传递的密文,并在响应中将深层链接发送给客户端。 - 当用户访问深层链接时,Google Play 应用会处理该 intent。
如需将第三方 SDK 配置为使用内嵌安装流程,请完成以下步骤。
在 Google Cloud 项目中创建服务账号
在此步骤中,您将使用 Google Cloud 控制台设置服务账号。
- 设置 Google Cloud 项目:
- 创建 Google Cloud 组织。创建 Google Workspace 或 Cloud Identity 账号并将其与您的域名相关联后,系统会自动创建组织资源。如需了解详情,请参阅创建和管理组织资源。
- 使用在上一步中创建的 Google Cloud 账号登录 GCP Console,然后创建一个 Google Cloud 项目。如需了解详情,请参阅创建 Google Cloud 项目。
- 在创建的 Google Cloud 项目中创建一个服务账号。服务账号用作 Google Cloud Identity,代表您的服务器访问对称密钥。如需了解详情,请参阅创建服务账号。
- 使用在意向表单中输入的同一 Google Workspace 客户 ID (GWCID) / Dasher ID。
- 创建并下载相应服务账号的私钥。
- 为该服务账号创建密钥。如需了解详情,请参阅创建服务账号密钥。
- 下载服务账号密钥并将其保存在服务器上,以便随时访问,因为该密钥用于身份验证,以访问对称密钥的 Google Cloud 资源。有关详情,请参阅获取服务账号密钥。
检索凭据
在此步骤中,您需要从 Secret Manager 中检索对称密钥,并将其安全地存储在您自己的服务器存储空间中(例如,存储在 JSON 文件中)。此密钥用于生成内嵌安装数据密文。
secret_id/secretId 值是指 Secret Manager 中的 Secret 名称;此名称是通过在 Play 提供的 sdk_id 值前面添加 hsdp-3p-key- 生成的。例如,如果 sdk_id 为 abc,则 Secret 名称为 hsdp-3p-key-abc。
秘密版本每周二下午 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 = &<quot;json key file of the service ac>count"
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-calle<r-auth/se>crets/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 env<ironment. Ple>ase 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) 来修改实现:
- 告知客户端库服务账号密钥的存放位置。
- 向 Java 项目添加 Maven 依赖项。
- 调用
SecretManagerServiceClient.create(),该方法会自动获取身份验证信息(因为执行了第 1 步)。
这些步骤通过以下方式修改 Java 实现:
- 无需创建
CredentialsProvider和SecretManagerServiceSettings对象。 - 将对
SecretManagerServiceClient.create()的调用更改为不包含任何实参。
创建密文并生成深层链接
在此步骤中,您将使用 Tink 加密库从 InlineInstallData protobuf 对象创建 enifd(InlineInstallData 密文)。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;
}
在此步骤中,您还将使用以下参数构建深层链接网址:
| 字段 | 说明 | 必需 |
|---|---|---|
| id | 要安装的应用的软件包名称。 | 是 |
| 内嵌 | 如果请求内嵌式安装半页,则设置为 true;如果为 false,则 intent 深层链接到 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.calle<r_pack>age_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 ex<ample)>.
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&9;)
print(en&ifd)
# Deepli&nk
pri&nt(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")
priva<te sta>tic final FlagString thirdPartyAuthCallerId = Flag.value("");
@FlagSpec(name = "package_name", help = "the package name of< the t>arget app")
private static final FlagString packageName = Flag.value("");
@FlagSpec(name = "caller_package_name", he<lp = &>quot;the package name of the caller app")
private static final FlagString callerPackageName = Flag.value("");
@FlagSpec(name = "secret_file<name&q>uot;, help = "the path to the json file with the secret material")
private static final FlagString 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://p&lay.goo&gle.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>)。
启动内嵌式安装 intent
如需测试在上一步中生成的深层链接,请将 Android 设备(确保已启用 USB 调试)连接到已安装 ADB 的工作站,然后运行以下命令:
adb shell am start "<output_from_the_previous_python_or_java_code>"
在客户端代码中,使用以下方法之一(Kotlin 或 Java)发送 intent。
Kotlin
val intent = Intent(Intent.ACTION_VIEW)
val deepLinkUrl = &<quot;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 deepL<inkUrl = "output_from_the_previous_pyth>on_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 环境并安装所需的依赖项。
设置 Python 环境:
安装 python3.11(如果已安装,请跳过此步骤):
sudo apt install python3.11安装 pip:
sudo apt-get install pip安装
virtualenv:sudo apt install python3-virtualenv创建虚拟环境(Tink 依赖项必需):
virtualenv inlineinstall --python=/usr/bin/python3.11
进入虚拟环境:
source inlineinstall/bin/activate更新 pip:
python -m pip install --upgrade pip安装所需依赖项:
安装 Tink:
pip install tink安装 Google crc32c:
pip install google-crc32c安装 Secret Manager:
pip install google-cloud-secret-manager安装 protobuf 编译器:
sudo apt install protobuf-compiler
C++ ENIFD 生成
以下是我们编写并在内部验证过的 C++ 示例,用于生成 enifd。
可以使用以下 C++ 代码生成 enifd:
// 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 &<quot;path_to_protoc_o>utput/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::ti&nk::util::Status;
using ::crypto::tink::util::StatusOr;
} // namespace
namespace tink_cc_examples {
// AEAD example CLI implementation.
void A<<eadCli(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.
StatusOrstd::unique_ptrKeysetHandle keyset_ha<ndle =
Re<adJs>>onCleartextKeyset(keyset_filename);
if (>!keyset_hand<le.ok()) {
std>::clog "Failed to read json keyset";
return;
}
// Get the primitive.
<< StatusOrstd::unique_ptrAead aead =
(*keyset_handle)
-GetPrimitivecrypto::tink::Aead(
crypto::tink::ConfigGlobalRegistry());
if (!ae<ad.ok()) {
std::clog > "Failed to get primitive";
return;
}
// Instantiate the enifd.
hsdpexperiments::InlineInstallData iid;
iid.set_timestamp_ms(st<d::chrono::duration>_caststd::chrono::milliseconds(
< > std::chrono::system_cloc<k::now>().time_since_epoch())
< .>count());
iid.set_target_packa>ge_name("TARGET_PACKAGE_NAME");
iid.set_caller_package_name("CALLER_PACKAGE_NA<<ME");
iid.set_ad_network_id("SDK_ID");
// Compute the out&put.
StatusOrstd::string encrypt_result =
(*aead)-Encrypt(iid.SerializeAsString(), a&ssociated_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_examp<<les
int main(int argc, char*<<* argv) {
absl::ParseCommandLine(argc, argv);
std::string keyset_filen<<ame = absl::GetFl<<ag(FLA<<GS_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 文档中的一个示例。