Partner Data Encryption & Secret Sharing Guide

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).

Quick Encrypt Tool (In-Browser)

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.

Encrypt Partner Credential
Client-Side WebCrypto Loading key...
Exact canonical Partner ID assigned by Google (bound as HPKE contextInfo).
Use Production for both prod and sandbox credentials unless instructed otherwise.
Processed strictly in local browser memory.

1. Overview & Security Architecture

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:

Zero-Touch Protection: Because encryption is asymmetric, partners do not need a pre-shared symmetric key. Secrets are encrypted with Google's public key on the partner side and can only be decrypted within Google's isolated production environment.

2. Public Encryption Key Specification

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".

HPKE X25519 Public Key Parameters
RFC 9180 RFC 8037 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

Example JWK Snippet from UCP Profile Manifest

{
  "kty": "OKP",
  "use": "enc",
  "kid": "PRmoFw",
  "crv": "X25519",
  "x": "wwgv_qEtAHgustNtA--NHjHfj1muFoGfM141Brj0hkc"
}

Retrieval & Discovery Endpoints

3. Step-by-Step Encryption Flow

  1. Fetch & Select Key: Retrieve the JSON document from https://ucp.goog/.well-known/ucp.json. Parse the keys array and select the item where use == "enc" (and optionally match the active kid).
  2. Decode Public Key Coordinate (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.
  3. Encrypt with HPKE:
    • Configure HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256, AEAD: AES-256-GCM.
    • Run HPKE single-shot encryption (Base Mode, RFC 9180 §5.1.1) over the secret string UTF-8 bytes.
    • Pass the UTF-8 bytes of your canonical 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).
  4. Format & Base64 Encode Ciphertext: Prepend the 5-byte Tink wire format prefix (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).
  5. Provide Encrypted Material: Provide the resulting Base64-encoded ciphertext string to Google as part of your partner credential onboarding or rotation workflow.

4. Integration Code Examples

Python

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}")

Go

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
}

Node.js / TypeScript

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");
}

Java

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);
  }
}

5. Providing the Encrypted Material

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.

Verification Checklist

6. Key Rotation & Lifecycle

Google periodically rotates encryption keys in accordance with cryptographic security best practices: