Technical instructions and in-browser utility for external partners and merchants to encrypt sensitive credentials (such as OAuth client secrets, API tokens, and webhook keys) using Google’s public HPKE key for secure transmission to Google Universal Commerce Protocol (UCP).
Use this client-side tool to encrypt your credential directly in your browser before submitting it via the Google onboarding form. Encryption runs locally in browser memory using the WebCrypto API (RFC 9180 HPKE); your plaintext secret is never transmitted over the network.
When external partners communicate sensitive authorization material to Google UCP, such as during merchant onboarding or credential rotation, Google supports application-layer encryption to ensure secrets remain protected end-to-end across trust boundaries.
Data encryption uses Hybrid Public Key Encryption (HPKE), standardized in RFC 9180. Under this model:
/.well-known/ucp.json).
Public encryption keys are published in the keys array of the UCP discovery manifest and versioned profiles. Partners should look for keys with "use": "enc" and "kty": "OKP".
| Parameter | Specification | Value / Identifier |
|---|---|---|
Key Type (kty) |
RFC 8037 Octet Key Pair | OKP |
Intended Use (use) |
Public Key Encryption | enc |
Elliptic Curve (crv) |
Curve25519 (X25519) | X25519 |
| KEM (Key Encapsulation) | DHKEM(X25519, HKDF-SHA256) | 0x0020 (KEM ID 32) |
| KDF (Key Derivation) | HKDF-SHA256 | 0x0001 (KDF ID 1) |
| AEAD (Authenticated Encryption) | AES-256-GCM | 0x0002 (AEAD ID 2) |
Key ID (kid) |
Base64URL-encoded 4-byte Tink key identifier | Dynamic identifier (e.g. PRmoFw) |
Public Coordinate (x) |
Base64URL-encoded (unpadded) raw 32-byte public key | Published in discovery JSON |
{
"kty": "OKP",
"use": "enc",
"kid": "PRmoFw",
"crv": "X25519",
"x": "wwgv_qEtAHgustNtA--NHjHfj1muFoGfM141Brj0hkc"
}
https://ucp.goog/.well-known/ucp.jsonhttps://ucp.goog/profiles/{VERSION}/ucp.json (e.g., 2026-08-25, 2026-04-08)https://ucp.goog/.well-known/ucp.json. Parse the keys array and select the item where use == "enc" (and optionally match the active kid).
x) & Key ID (kid): Decode the Base64URL-encoded string in property x to obtain the 32 raw public key bytes, and decode kid to obtain the 4-byte Tink key ID.
KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256, AEAD: AES-256-GCM.partner_id (the unique partner identifier assigned to you by Google during onboarding) for info / application context (contextInfo) to cryptographically bind the ciphertext to your partner identity, and empty bytes ("") for aad (associated data).0x01 followed by the 4-byte decoded kid; Google Tink adds this prefix automatically when configured with OutputPrefixType.TINK) before the 32-byte encapsulated public key and the AEAD ciphertext/tag, then encode the combined bytes using standard Base64 (or URL-safe Base64).
Using a standard RFC 9180 HPKE library (pip install pyhpke):
import base64
import json
import urllib.request
from hpke import AEADId, CipherSuite, KDFId, KEMId
# 1. Fetch UCP Discovery Profile to get active encryption key
req = urllib.request.urlopen("https://ucp.goog/.well-known/ucp.json")
profile = json.loads(req.read().decode("utf-8"))
# 2. Extract the encryption key (use == "enc" and kty == "OKP")
enc_key = next(k for k in profile.get("keys", []) if k.get("use") == "enc" and k.get("kty") == "OKP")
x_coord_bytes = base64.urlsafe_b64decode(enc_key["x"] + "==")
kid_bytes = base64.urlsafe_b64decode(enc_key["kid"] + "==")
# 3. Encrypt secret with HPKE (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256, AES_256_GCM)
suite_x25519 = CipherSuite(KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM)
partner_id = "PARTNER_UNIQUE_ID"
plaintext_secret = "my_partner_oauth_client_secret_xyz"
encapsulated_key, ciphertext = suite_x25519.setup_sender_and_seal(
public_key=x_coord_bytes,
info=partner_id.encode("utf-8"),
aad=b"",
plaintext=plaintext_secret.encode("utf-8")
)
# Prepend the 5-byte Tink wire format prefix (0x01 + 4-byte decoded kid)
tink_prefix = b"\x01" + kid_bytes
raw_ciphertext = tink_prefix + encapsulated_key + ciphertext
# 4. Base64 encode for transmission
encoded_secret = base64.b64encode(raw_ciphertext).decode("utf-8")
print(f"Encrypted Secret: {encoded_secret}")
Using Google Tink Go (github.com/tink-crypto/tink-go/v2):
package main
import (
"context"
"encoding/base64"
"fmt"
"github.com/tink-crypto/tink-go/v2/hybrid"
"github.com/tink-crypto/tink-go/v2/keyset"
)
// EncryptSecret encrypts a raw client secret with a Tink HPKE public keyset handle,
// binding the ciphertext to partnerID via contextInfo.
func EncryptSecret(ctx context.Context, pubHandle *keyset.Handle, secret string, partnerID string) (string, error) {
encrypter, err := hybrid.NewHybridEncrypt(pubHandle)
if err != nil {
return "", fmt.Errorf("failed to create hybrid encrypter: %w", err)
}
ciphertext, err := encrypter.Encrypt([]byte(secret), []byte(partnerID))
if err != nil {
return "", fmt.Errorf("failed to encrypt secret: %w", err)
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
Using @hpke/core:
import { Aes256Gcm, CipherSuite, DhkemX25519HkdfSha256, HkdfSha256 } from "@hpke/core";
async function encryptUcpSecret(
secretText: string,
partnerId: string,
rawPublicKeyBase64Url: string,
kidBase64Url: string,
): Promise<string> {
// Decode base64url X coordinate (32-byte public key) and kid (4-byte Tink key ID)
const rawKey = Buffer.from(rawPublicKeyBase64Url, "base64url");
const kidBytes = Buffer.from(kidBase64Url, "base64url");
const suite = new CipherSuite({
kem: new DhkemX25519HkdfSha256(),
kdf: new HkdfSha256(),
aead: new Aes256Gcm(),
});
const publicKey = await suite.kem.importKey("raw", rawKey, true);
const sender = await suite.createSenderContext({
recipientPublicKey: publicKey,
info: new TextEncoder().encode(partnerId),
});
const ciphertext = await sender.seal(new TextEncoder().encode(secretText));
// Combine 5-byte Tink prefix (0x01 + 4-byte kid) + encapsulated key + ciphertext
const tinkPrefix = new Uint8Array(1 + kidBytes.length);
tinkPrefix[0] = 0x01;
tinkPrefix.set(kidBytes, 1);
const combined = new Uint8Array(
tinkPrefix.byteLength + sender.enc.byteLength + ciphertext.byteLength,
);
combined.set(tinkPrefix, 0);
combined.set(new Uint8Array(sender.enc), tinkPrefix.byteLength);
combined.set(
new Uint8Array(ciphertext),
tinkPrefix.byteLength + sender.enc.byteLength,
);
return Buffer.from(combined).toString("base64");
}
Using Google Tink Java:
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.hybrid.HybridConfig;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class UcpEncryptor {
static {
try {
HybridConfig.register();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String encryptSecret(KeysetHandle publicKeysetHandle, String secret, String partnerId) throws Exception {
HybridEncrypt encryptor = publicKeysetHandle.getPrimitive(HybridEncrypt.class);
byte[] ciphertext = encryptor.encrypt(
secret.getBytes(StandardCharsets.UTF_8),
partnerId.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(ciphertext);
}
}
Once you have encrypted your credential using the steps above, provide the Base64-encoded ciphertext string along with your partner_id (the canonical partner identifier assigned by Google during onboarding) and the public key identifier (kid) used for encryption to Google through your designated partner onboarding or credential rotation channel.
0x01 followed by the 4-byte decoded kid), the 32-byte encapsulated public key, and the AES-256-GCM ciphertext and 16-byte authentication tag."use": "enc", "kty": "OKP") published in https://ucp.goog/.well-known/ucp.json.DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-256-GCM.info (contextInfo) parameter matches the exact UTF-8 bytes of your canonical partner_id assigned by Google during onboarding.Google periodically rotates encryption keys in accordance with cryptographic security best practices: