راه‌اندازی IP ناشرداده

پراکسی‌های حریم خصوصی مرورگر و تنظیمات محافظت از IP می‌توانند نشانی‌های IP کارخواه را در درخواست‌های آگهی طرف سوم به Google Ad Manager پنهان کنند. نشانی‌های IP پنهان‌شده می‌توانند دقت هدف‌یابی جغرافیایی و مرتبط بودن آگهی را کاهش دهند. چون سرور وب یا شبکه توزیع محتوای (CDN) شما در بافت طرف اول با کاربر تعامل دارد، سرور شما نشانی IP پنهان‌نشده کاربر را دریافت می‌کند.

ویژگی IP ارائه‌شده توسط ناشر به شما امکان می‌دهد نشانی IPv4 یا IPv6 پنهان‌نشده کاربر را در سرور خود ضبط کنید، نشانی را بااستفاده از «رمزگذاری کلید عمومی ترکیبی» (HPKE) رمزگذاری کنید، و بار داده رمزگذاری‌شده را به «برچسب ناشر Google» (GPT) در سمت کارخواه ارسال کنید.

این صفحه ادغام IP ارائه‌شده توسط ناشر با GPT را پوشش می‌دهد.

پیاده‌سازی گردش کار سمت سرور

برای جلوگیری از افشای نشانی IP متن ساده کاربر برای اسکریپت‌های طرف سوم در مرورگر، مراحل زیر را در کارگزار وب یا کارگر لبه CDN خود انجام دهید:

  • همه ضبط‌های IP را انجام دهید.
  • کلید عمومی را واکشی کنید.
  • رمزگذاری HPKE را انجام می‌دهد.

ضبط نشانی IP کارخواه

نشانی IPv4 یا IPv6 پنهان‌نشده مشتری را از درخواست صفحه ورودی در سرور وب یا CDN خود استخراج کنید:

  • اتصال‌های مستقیم: نشانی سوکت راه دور اتصال HTTP ورودی را می‌خواند.
  • پشت CDN، متعادل‌کننده بار، یا پراکسی معکوس: سرصفحه IP کارخواه را از پراکسی لبه مورداعتماد خود بخوانید، مانند سرصفحه X-Forwarded-For یا سرصفحه IP کارخواه مختص CDN. فقط به سرایندهای بازارسالی اعتماد کنید که زیرساخت پراکسی شما اضافه یا لغو می‌کند. وقتی سرصفحه X-Forwarded-For حاوی چندین نشانی جداشده با کاما است، IP مشتری را که پراکسی لبه مورداعتماد اضافه کرده است انتخاب کنید.

Ad Manager از نشانی‌های زیر پشتیبانی می‌کند:

  • پروتکل اینترنتی نسخه ۴، برای مثال، 203.0.113.42.
  • برای مثال، IPv6،‏ 2001:db8::8a2e:370:7334.

فقط رشته نشانی IP را بدون شماره درگاه، براکت، پیشوند CIDR، یا فاصله اطراف آن ارسال کنید.

واکشی و ذخیره کردن کلید عمومی در حافظه نهان

Google کلید رمزگذاری HPKE فعال را به‌عنوان RFC 8037 JSON Web Key Set (JWKS) در نقطه پایانی زیر منتشر می‌کند:

https://www.gstatic.com/real-ip/ip_enc_key.json

درخواست GET به این نقطه پایانی یک شیء JSON برمی‌گرداند که آرایه keys آن شامل یک شیء کلید فعال است. از کلید موجود در ویژگی x آن شیء برای رمزگذاری نشانی IP کارخواه استفاده کنید.

مثال زیر کلید عمومی را واکشی می‌کند و ویژگی x را درصورت موفقیت استخراج می‌کند و درصورت عدم موفقیت استثنایی را ایجاد می‌کند:

جاوا

private static final String KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json";

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * <p>In production, cache the returned public key in memory and refresh it at least once every 24
 * hours rather than fetching it on every incoming request.
 *
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
public static String fetchPublicKey() throws IOException, InterruptedException {
  return fetchPublicKey(KEY_ENDPOINT_URL);
}

public static String fetchPublicKey(String endpointUrl) throws IOException, InterruptedException {
  HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
  HttpRequest request =
      HttpRequest.newBuilder()
          .uri(URI.create(endpointUrl))
          .timeout(Duration.ofSeconds(5))
          .GET()
          .build();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  if (response.statusCode() < 200 || response.statusCode() >= 300) {
    throw new IOException("Failed to fetch public key, HTTP status: " + response.statusCode());
  }
  JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
  JsonArray keys = json.getAsJsonArray("keys");
  if (keys == null || keys.isEmpty()) {
    throw new IOException("No keys found in response from " + endpointUrl);
  }
  JsonObject keyObj = keys.get(0).getAsJsonObject();
  if (!keyObj.has("x")) {
    throw new IOException("Key payload missing 'x' property: " + keyObj);
  }
  return keyObj.get("x").getAsString();
}

Python

KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json"


def fetch_public_key(url: str = KEY_ENDPOINT_URL) -> str:
  """Fetches the active X25519 public key from Google's key endpoint.

  In production, cache the returned public key in memory and refresh it at
  least once every 24 hours rather than fetching it on every incoming request.

  Args:
    url: URL of the public key endpoint.

  Returns:
    The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
  """
  with urllib.request.urlopen(url, timeout=5) as response:
    data = json.loads(response.read().decode("utf-8"))
  keys = data.get("keys", [])
  if not keys:
    raise ValueError(f"No keys found in response from {url}")
  key_payload = keys[0].get("x")
  if not key_payload:
    raise ValueError(f"Key payload missing 'x' property: {keys[0]}")
  return key_payload


JavaScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param {string=} url URL of the public key endpoint.
 * @return {!Promise<string>} The unpadded Base64URL-encoded 32-byte X25519
 *     public key ('x').
 */
async function fetchPublicKey(url = KEY_ENDPOINT_URL) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = await response.json();
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

TypeScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

interface JwkKey {
  kty?: string;
  crv?: string;
  x?: string;
  kid?: string;
}

interface KeyEndpointResponse {
  keys?: JwkKey[];
}

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param url URL of the public key endpoint.
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
export async function fetchPublicKey(
  url: string = KEY_ENDPOINT_URL,
): Promise<string> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = (await response.json()) as KeyEndpointResponse;
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

اگر موفقیت‌آمیز باشد، پاسخ شامل آرایه keys است که حاوی شیء کلید با ویژگی x است:

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "X25519",
      "x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
      "kid": "EZMLFw"
    }
  ]
}

جدول زیر ویژگی‌های شیء کلید را در آرایه keys شرح می‌دهد:

دارایی شرح
crv شناسه منحنی بیضوی. همیشه X25519.
kid شناسه کلید کدبندی‌شده base64url بدون فاصله، که وقتی Google کلید را می‌چرخاند تغییر می‌کند.
kty نوع کلید. همیشه OKP (جفت کلید هشت‌تایی).
x کلید عمومی X25519 خام ۳۲ بایتی کدبندی‌شده با base64url بدون لایه که برای رمزگذاری استفاده می‌شود.

کلید عمومی واکشی‌شده را در حافظه سرورتان ذخیره کنید و کلید را حداقل هر ۲۴ ساعت یک‌بار، یا زودتر طبق سرایند پاسخ HTTP Cache-Control، بازآوری کنید. کلید عمومی را در هر درخواست صفحه ورودی واکشی نکنید. Google کلیدهای خصوصی چرخشی را حفظ می‌کند. بارداده‌های رمزگذاری‌شده با کلید عمومی که در ۲۴ ساعت گذشته ذخیره‌شده‌اند، درطول چرخه کلید بدون وقفه به رمزگشایی ادامه می‌دهند.

رمزگذاری نشانی IP

نمایش بایت UTF-8 رشته نشانی IP کارخواه را بااستفاده از HPKE (RFC 9180) و کلید عمومی ۳۲ بایتی X25519 رمزگشایی‌شده، x، که در واکشی و ذخیره کلید عمومی در حافظه نهان بازیابی شده است رمزگذاری کنید.

مثال زیر رشته نشانی IP را رمزگذاری می‌کند:

جاوا

این مثال از Tink (com.google.crypto.tink:tink نسخه ۱.۱۶.۰ یا بالاتر) استفاده می‌کند:

static {
  try {
    HybridConfig.register();
  } catch (GeneralSecurityException e) {
    throw new ExceptionInInitializerError(e);
  }
}

/**
 * Encrypts an IP address using HPKE (DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
@AccessesPartialKey
public static String encryptIp(String pubXB64Url, String ipStr) throws GeneralSecurityException {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  byte[] pubBytes = Base64.getUrlDecoder().decode(pubXB64Url);
  if (pubBytes.length != 32) {
    throw new IllegalArgumentException(
        "Public key must be exactly 32 bytes, got " + pubBytes.length);
  }

  // 2. Configure HPKE parameters (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM, NO_PREFIX).
  HpkeParameters parameters =
      HpkeParameters.builder()
          .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
          .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
          .setAeadId(HpkeParameters.AeadId.AES_256_GCM)
          .setVariant(HpkeParameters.Variant.NO_PREFIX)
          .build();

  HpkePublicKey publicKey =
      HpkePublicKey.create(parameters, Bytes.copyFrom(pubBytes), /* idRequirement= */ null);

  // 3. Encrypt the UTF-8 IP address bytes. With Variant.NO_PREFIX, Tink
  //    outputs raw (enc || ciphertext_with_tag).
  KeysetHandle handle =
      KeysetHandle.newBuilder()
          .addEntry(KeysetHandle.importKey(publicKey).withRandomId().makePrimary())
          .build();

  HybridEncrypt encryptor = handle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class);
  byte[] payload =
      encryptor.encrypt(ipStr.getBytes(StandardCharsets.UTF_8), /* contextInfo= */ new byte[0]);

  // 4. Return unpadded Base64URL-encoded payload.
  return Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
}

Python

این مثال از pyhpke استفاده می‌کند:

def encrypt_ip(pub_x_b64url: str, ip_str: str) -> str:
  """Encrypts an IP address using HPKE (RFC 9180).

  Args:
    pub_x_b64url: Base64URL-encoded 32-byte X25519 public key ('x').
    ip_str: Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").

  Returns:
    Unpadded Base64URL-encoded payload (enc || ciphertext).
  """
  # 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  pub_bytes = base64.urlsafe_b64decode(
      pub_x_b64url + "=" * (-len(pub_x_b64url) % 4)
  )
  if len(pub_bytes) != 32:
    raise ValueError(
        f"Public key must be exactly 32 bytes, got {len(pub_bytes)}"
    )

  # 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  #    AES256_GCM).
  suite = CipherSuite.new(
      KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM
  )
  public_key = suite.kem.deserialize_public_key(pub_bytes)

  # 3. Create a fresh sender context per encryption (you may cache 'suite' and
  #    'public_key', but never reuse 'sender' across multiple seal() calls) and
  #    package (enc || ciphertext).
  enc, sender = suite.create_sender_context(public_key)
  ciphertext = sender.seal(ip_str.encode("utf-8"))

  # 4. Return unpadded Base64URL-encoded payload.
  return base64.urlsafe_b64encode(enc + ciphertext).decode("utf-8").rstrip("=")


JavaScript

این مثال از @hpke/core استفاده می‌کند:

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param {string} pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param {string} ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return {!Promise<string>} Unpadded Base64URL-encoded payload
 *     (enc || ciphertext).
 */
async function encryptIp(pubXB64Url, ipStr) {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

TypeScript

این مثال از @hpke/core استفاده می‌کند:

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
export async function encryptIp(
  pubXB64Url: string,
  ipStr: string,
): Promise<string> {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

کتابخانه HPKE خود را با مجموعه رمز و پارامترهای زیر پیکربندی کنید:

پارامتر مقدار
مکانیزم کپسوله‌سازی کلید (KEM) ‫DHKEM(X25519, HKDF-SHA256)‏ (0x0020)
تابع استخراج کلید (KDF) ‫HKDF-SHA256‏ (0x0001)
رمزگذاری اصالت‌سنجی‌شده با داده‌های مرتبط (AEAD) ‫AES-256-GCM‏ (0x0002)
حالت پایه (0x00)
اطلاعات برنامه (info) خالی ("")
داده‌های مرتبط (aad) خالی ("")

برونداد رمزگذاری‌شده را به‌عنوان الحاق خام کلید عمومی زودگذر کپسوله‌شده ۳۲ بایتی، enc، و متن رمز AEAD، شامل برچسب اصالت‌سنجی GCM‏ ۱۶ بایتی، بدون Tink یا پیشوند سرصفحه سفارشی قالب‌بندی کنید. نتیجه را به‌صورت رشته base64url بدون لایه کدبندی کنید.

هر بار داده را به‌عنوان پیام HPKE تک‌باره مستقل با جفت کلید موقت جدید رمزگذاری کنید. اگر کتابخانه HPKE شما از بافت فرستنده باحالت استفاده می‌کند، برای هر رمزگذاری بافت جدیدی ایجاد کنید و از آن در چندین پیام استفاده مجدد نکنید. نشانی IP کارخواه را برای هر درخواست صفحه رمزگذاری کنید، یا رشته متن رمزی base64url حاصل را برای هر نشانی IP برای مدت کوتاهی (TTL) ذخیره کنید.

پیاده‌سازی گردش کار سمت کارخواه

پس‌از اینکه سرورتان بار داده IP رمزگذاری‌شده base64url را تولید کرد، قبل‌از درخواست آگهی، بار داده را به GPT در کارخواه ارسال کنید.

ارسال IP رمزگذاری‌شده به GPT

رشته IP رمزگذاری‌شده را بااستفاده از ویژگی ipAddress در PageSettingsConfig به روش googletag.setConfig منتقل کنید.

مثال زیر بار داده IP رمزگذاری‌شده تولیدشده توسط سرور را قبل‌از فعال کردن سرویس‌ها و نمایش جایگاه آگهی، در روش googletag.setConfig درج می‌کند:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Publisher-provided IP example</title>
    <script src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous" async></script>
    <script>
      window.googletag = window.googletag || {cmd: []};

      // Inject the base64url-encoded encrypted IP generated by your server.
      const encryptedClientIp = 'ENCRYPTED_IP_PAYLOAD';

      googletag.cmd.push(function() {
        // Configure the encrypted IP before enabling services.
        googletag.setConfig({
          ipAddress: encryptedClientIp,
        });

        googletag
            .defineSlot('/6355419/Travel/Europe/France/Paris', [300, 250], 'banner-ad')
            .addService(googletag.pubads());
        googletag.enableServices();
      });
    </script>
  </head>
  <body>
    <div id="banner-ad" style="width: 300px; height: 250px;"></div>
    <script>
      googletag.cmd.push(function() {
        googletag.display('banner-ad');
      });
    </script>
  </body>
</html>

‫ENCRYPTED_IP_PAYLOAD را با رشته کدبندی‌شده base64url بدون لایه که سرورتان تولید می‌کند جایگزین کنید.

برای پاک کردن نشانی IP پیکربندی‌شده قبلی در صفحه، ویژگی ipAddress را روی مقدار null تنظیم کنید:

googletag.setConfig({ ipAddress: null });

درستی‌سنجی کردن ادغام

برای تأیید اینکه GPT بار داده IP رمزگذاری‌شده شما را ارسال می‌کند و Ad Manager بار داده را رمزگشایی می‌کند، از ابزارهای توسعه‌دهنده مرورگرتان و Google Publisher Console استفاده کنید:

ابزارهای توسعه‌دهنده مرورگر

  1. ابزارهای توسعه‌دهنده مرورگرتان را باز کنید.
  2. برگه شبکه را انتخاب کنید.
  3. فیلتر کردن درخواست‌های آگهی به /gampad/ads.
  4. تأیید کنید که نشانی وب درخواست شامل پارامتر پُرسمان ips غیرخالی باشد. چون GPT مقدار ipAddress را قبل‌از ارسال درخواست آگهی بیشتر کدگذاری می‌کند، پارامتر ips با رشته رمزگذاری‌شده شما مطابقت ندارد.

کنسول ناشر Google

  1. برای باز کردن کنسول، مراحل موجود در راهنمای کنسول ناشر Google را دنبال کنید.
  2. برگه درخواست صفحه را انتخاب کنید.
  3. مطمئن شوید که هیچ هشدار ipAddress نشان داده نمی‌شود. اگر مشکلی در بار مفید وجود داشته باشد، کنسول یکی از هشدارهای زیر را ثبت می‌کند:

    • اگر مقدار ارسال‌شده به ویژگی ipAddress یک رشته base64 یا base64url معتبر بین ۶۷ تا ۱۲۴ نویسه نباشد، GPT مقدار را نادیده می‌گیرد، پارامتر ips را از درخواست آگهی حذف می‌کند، و هشدار زیر را ثبت می‌کند:

      Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.
      
    • اگر Ad Manager نتواند بار داده IP ارائه‌شده را رمزگشایی کند، یا اگر متن ساده رمزگشایی‌شده نشانی IPv4 یا IPv6 معتبری نباشد، کنسول هشدار زیر را ثبت می‌کند و به نشانی IP درخواست شبکه برمی‌گردد:

      Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).