Cette page décrit comment les SDK tiers peuvent intégrer inline install, une nouvelle fonctionnalité de test pour Google Play qui présente les détails du produit de l'application Google Play dans une interface demi-feuille. L'installation intégrée permet aux utilisateurs de bénéficier d'un flux d'installation d'application fluide sans quitter le contexte de l'application.
Les développeurs de SDK tiers peuvent intégrer la fonctionnalité d'installation intégrée à leurs SDK pour permettre aux développeurs d'applications qui utilisent ces SDK d'accéder aux installations intégrées pour leurs applications.
Conditions requises
Pour que l'interface de feuille à moitié hauteur d'installation intégrée s'affiche dans une application :
- La version minimale de Google Play doit être 40.4.
- Le niveau d'API Android doit être 23 ou supérieur.
Architecture du processus
L'architecture du processus d'installation intégrée est illustrée dans la figure suivante :
- Les serveurs Google Play génèrent des clés de chiffrement AEAD (Authenticated Encryption with Associated Data) et les ingèrent dans une instance Secret Manager de Google Cloud Platform (GCP).
- L'intégrateur tiers récupère la clé AEAD depuis GCP Secret Manager.
- L'intégrateur tiers chiffre les données d'installation intégrée
Intent, génère le texte chiffré transmis dans le lien profond utilisé pour appeler l'intent d'installation intégrée et envoie des liens profonds au client dans les réponses. - Lorsque le lien profond est suivi, l'application Google Play gère l'intention.
Pour configurer un SDK tiers afin qu'il utilise le processus d'installation intégrée, procédez comme suit.
Créer des comptes de service dans un projet Google Cloud
Dans cette étape, vous allez configurer un compte de service à l'aide de la console Google Cloud.
- Configurez un projet Google Cloud :
- Créez une organisation Google Cloud. Lorsque vous créez un compte Google Workspace ou Cloud Identity et que vous l'associez à votre nom de domaine, la ressource Organisation est automatiquement créée. Pour en savoir plus, consultez Créer et gérer des ressources d'organisation.
- Connectez-vous à la console GCP à l'aide du compte Google Cloud créé à l'étape précédente, puis créez un projet Google Cloud. Pour en savoir plus, consultez Créer un projet Google Cloud.
- Créez un compte de service dans le projet Google Cloud créé. Le compte de service est utilisé comme une identité Google Cloud pour accéder à la clé symétrique au nom de vos serveurs. Pour en savoir plus, consultez Créer un compte de service.
- Utilisez le même numéro client Google Workspace (GWCID) ou ID Dasher que celui saisi dans le formulaire d'intérêt.
- Créez et téléchargez la clé privée de ce compte de service.
- Créez une clé pour ce compte de service. Pour en savoir plus, consultez Créer une clé de compte de service.
- Téléchargez la clé du compte de service et conservez-la accessible sur votre serveur, car elle est utilisée pour l'authentification afin d'accéder aux ressources Google Cloud pour les clés symétriques. Pour plus de détails, voir Obtenir une clé de compte de service.
Récupérer les identifiants
Dans cette étape, vous récupérez la clé symétrique depuis Secret Manager et la stockez de manière sécurisée (par exemple, dans un fichier JSON) sur le stockage de votre propre serveur. Cette clé est utilisée pour générer le texte chiffré des données d'installation intégrée.
Les valeurs secret_id/secretId font référence au nom secret dans Secret Manager. Ce nom est généré en ajoutant le préfixe hsdp-3p-key- à la valeur sdk_id fournie par Play. Par exemple, si sdk_id est abc, le nom secret est hsdp-3p-key-abc.
Les versions secrètes sont mises à jour toutes les semaines, le mardi à 14h UTC. Les clés les plus récentes restent fonctionnelles jusqu'à la prochaine rotation, et les matériaux clés doivent être renouvelés et stockés chaque semaine.
Exemple Python
L'exemple de code suivant utilise un jeton d'accès stocké dans un fichier JSON pour accéder au matériel de clé dans GCP Secret Manager et l'imprimer dans la console.
#!/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)
Exemple Java
L'exemple de code suivant utilise un jeton d'accès stocké dans un fichier JSON pour accéder au matériel de clé dans GCP Secret Manager et l'écrire dans un fichier 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();
}
}
}
}
Définir les identifiants par défaut de l'application
Si vous ne souhaitez pas utiliser CredentialsProvider pour transmettre la clé privée à un fichier JSON dans l'implémentation Java, vous pouvez modifier l'implémentation en définissant les identifiants par défaut de l'application (ADC) :
- Indiquez aux bibliothèques clientes où trouver la clé du compte de service.
- Ajouter les dépendances Maven au projet Java.
- Appelez
SecretManagerServiceClient.create(), qui récupère automatiquement l'authentification (grâce à l'étape 1).
Ces étapes modifient l'implémentation Java en :
- Il n'est donc plus nécessaire de créer les objets
CredentialsProvideretSecretManagerServiceSettings. - Modifiez l'appel à
SecretManagerServiceClient.create()pour n'inclure aucun argument.
Créer un texte chiffré et générer un lien profond
Dans cette étape, vous utilisez la bibliothèque de cryptographie Tink pour créer le enifd (InlineInstallData texte chiffré) à partir de l'objet protobuf InlineInstallData.
Le prototype InlineInstallData est défini comme suit :
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;
}
Au cours de cette étape, vous allez également créer l'URL du lien profond à l'aide de ces paramètres :
| Champs | Description | Obligatoire |
|---|---|---|
| id | Le nom du package de l'application à installer. | Oui |
| intégrée | Définir sur true si une demi-feuille d'installation en ligne est demandée ; si false, l'intention est un lien profond vers Google Play. |
Oui |
| enifd | Identifiant chiffré pour les SDK tiers. | Oui |
| lft | Un identifiant interne. | Oui |
| 3pAuthCallerId | Identifiant du SDK. | Oui |
| fiche | Paramètre facultatif permettant de spécifier la cible d'une fiche Play Store personnalisée. | Non |
| URL de provenance | Chaîne de suivi referrer facultative. | Non |
Exemple Python
La commande suivante génère du code Python à partir de InlineInstallData.proto :
protoc InlineInstallData.proto --python_out=.
L'exemple de code Python suivant construit InlineInstallData et le chiffre avec la clé symétrique pour créer le texte chiffré :
#!/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}")
Exécutez le script Python en exécutant la commande suivante :
python <file_name>.py
Exemple Java
La commande suivante génère du code Java à partir de InlineInstallData.proto :
protoc InlineInstallData.proto --java_out=.
L'exemple de code Java suivant construit InlineInstallData et le chiffre avec la clé symétrique pour créer le texte chiffré :
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);
}
}
Enfin, compilez le programme Java en un fichier binaire et exécutez-le à l'aide du code suivant :
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>
- L'indicateur
secret_filenamespécifie le chemin d'accès au fichier JSON contenant le matériel secret. - L'indicateur
package_namecorrespond à l'ID de document de l'application cible. - L'option
third_party_idpermet de spécifier l'ID d'authentification de l'appelant tiers (c'est-à-dire<sdk_id>).
Lancer l'intent d'installation intégrée
Pour tester le lien profond généré lors de l'étape précédente, connectez un appareil Android (assurez-vous que le débogage USB est activé) à un poste de travail sur lequel ADB est installé et exécutez la commande suivante :
adb shell am start "<output_from_the_previous_python_or_java_code>"
enifd
Dans le code client, envoyez l'intention à l'aide de l'une des méthodes suivantes (Kotlin ou 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.
}
Annexe
Les sections suivantes fournissent des conseils supplémentaires sur certains cas d'utilisation.
Préparer l'environnement Python
Pour exécuter l'exemple de code Python, configurez l'environnement Python sur votre poste de travail et installez les dépendances requises.
Configurez l'environnement Python :
Installez python3.11 (si déjà installé, ignorez cette étape) :
sudo apt install python3.11Installez pip :
sudo apt-get install pipInstaller
virtualenv:sudo apt install python3-virtualenvCréez un environnement virtuel (requis pour la dépendance Tink) :
virtualenv inlineinstall --python=/usr/bin/python3.11
Accédez à l'environnement virtuel :
source inlineinstall/bin/activateMettez à jour pip :
python -m pip install --upgrade pipInstallez les dépendances requises :
Installez Tink :
pip install tinkInstallez Google crc32c :
pip install google-crc32cInstaller Secret Manager :
pip install google-cloud-secret-managerInstallez le compilateur protobuf :
sudo apt install protobuf-compiler
Génération d'enifd C++
Voici un exemple en C++ que nous avons écrit et validé en interne pour générer le enifd.
La génération de enifd peut être effectuée à l'aide du code C++ suivant :
// 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;
}
Ce code a été adapté d'un sample que l'on peut trouver dans la documentation de Tink.