Partner Data Encryption & Secret Sharing Guide

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

1. Overview & Security Architecture

When external partners communicate sensitive authorization material to Google UCP—such as during third-party merchant onboarding, credential rotation, or internal gateway provisioning—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) Keystore Key Identifier Dynamic hash (e.g. 7Us51w)
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": "7Us51w",
  "crv": "X25519",
  "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeURbgjeVCz--zVc"
}

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): Decode the Base64URL-encoded string in property x to obtain the 32 raw public key bytes.
  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 empty bytes ("") for info / application context and aad (associated data).
  4. Base64 Encode Ciphertext: Encode the resulting ciphertext bytes (consisting of the encapsulated public key followed by the AEAD ciphertext and tag) using standard Base64 (or URL-safe Base64).
  5. Submit in Request: Transmit the Base64 string in the externally_encrypted_client_secret field of your API request payload.

4. Integration Code Examples

Python

Using the official Google Tink library (pip install tink) or standard cryptography libraries:

import base64
import json
import urllib.request
import tink
from tink import hybrid

# 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"] + "==")

# 3. Encrypt secret with Tink HPKE (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256, AES_256_GCM)
hybrid.register()
# Example with standard HPKE library:
from hpke import CipherSuite, KEMId, KDFId, AEADId

suite_x25519 = CipherSuite(KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM)

plaintext_secret = "my_partner_oauth_client_secret_xyz"
encapsulated_key, ciphertext = suite_x25519.setup_sender_and_seal(
    public_key=x_coord_bytes,
    info=b"",
    aad=b"",
    plaintext=plaintext_secret.encode("utf-8")
)
raw_ciphertext = encapsulated_key + ciphertext

# 4. Base64 encode for transmission
encoded_secret = base64.b64encode(raw_ciphertext).decode("utf-8")
print(f"Externally 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.
func EncryptSecret(ctx context.Context, pubHandle *keyset.Handle, secret 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), nil)
	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, rawPublicKeyBase64Url: string): Promise<string> {
  // Decode base64url X coordinate to raw 32-byte public key
  const rawKey = Buffer.from(rawPublicKeyBase64Url, "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,
  });

  const ciphertext = await sender.seal(new TextEncoder().encode(secretText));

  // Combine encapsulated key + ciphertext
  const combined = new Uint8Array(sender.enc.byteLength + ciphertext.byteLength);
  combined.set(new Uint8Array(sender.enc), 0);
  combined.set(new Uint8Array(ciphertext), 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) throws Exception {
    HybridEncrypt encryptor = publicKeysetHandle.getPrimitive(HybridEncrypt.class);
    byte[] ciphertext = encryptor.encrypt(secret.getBytes(StandardCharsets.UTF_8), null);
    return Base64.getEncoder().encodeToString(ciphertext);
  }
}

5. API Transmission & Verification

When making requests to Google UMIS (e.g. EncryptClientSecret), supply the encrypted secret using the externally_encrypted_client_secret property:

Example Request Payload

POST /rpc/commerce.delivery.merchantintegrations.umis.internal.DebugService/EncryptClientSecret
Content-Type: application/json

{
  "partner_id": "PARTNER_UNIQUE_ID",
  "externally_encrypted_client_secret": "CpYBCokBCjV0eXBlLmdvb2dsZWFwaXMuY29t..."
}

Expected Success Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "encrypted_client_secret": "encrypted_secret=...=PARTNER_UNIQUE_ID"
}

Error Responses & Troubleshooting

Status Code Condition Resolution
400 INVALID_ARGUMENT Partner ID is missing, or neither client_secret nor externally_encrypted_client_secret was provided. Provide a valid partner_id and ensure the payload contains externally_encrypted_client_secret.
500 INTERNAL Ciphertext decryption failed. Verify that: (1) the ciphertext is valid Base64, (2) the active public key from ucp.json was used, (3) HPKE parameters match DHKEM(X25519) / SHA-256 / AES-256-GCM.

6. Key Rotation & Lifecycle

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