解密比邻商家定位信号

如果发布商将比邮政编码更具体的移动位置数据传递给 Authorized Buyers,Authorized Buyers 将在新的加密字段 BidRequest.encrypted_hyperlocal_set 中向买家发送地理围栏。BidRequest.encrypted_hyperlocal_set

时间轴

  1. 用户安装含广告的移动应用,并同意该应用访问设备位置信息并将其与第三方共享。此应用还与 Google Ads SDK 集成,并将此设备的位置信息发送给 Google。
  2. Google 服务器会生成一个特殊的超本地定位信号,代表设备位置周围的地理围栏(例如,为了保护用户的隐私)。
  3. Google 服务器使用特定于每个买方的安全密钥来对比邻商家定位信号进行序列化和加密。请注意,您的出价工具依靠相同的密钥来解密 WINNING_PRICE 宏
  4. 您的出价工具会将比邻商家定位信号解密并反序列化为协议缓冲区。然后,您的出价工具就可以分析信号和出价。

依赖项

您需要一个支持 SHA-1 HMAC 的加密库,如 Openssl

定义

proto 中定义了比邻商家定位信号,具体如下:

// A hyperlocal targeting location when available.
//
message Hyperlocal {
  // A location on the Earth's surface.
  //
  message Point {
    optional float latitude = 1;
    optional float longitude = 2;
  }

  // The mobile device can be at any point inside the geofence polygon defined
  // by a list of corners.  Currently, the polygon is always a parallelogram
  // with 4 corners.
  repeated Point corners = 1;
}

message HyperlocalSet {
  // This field currently contains at most one hyperlocal polygon.
  repeated Hyperlocal hyperlocal = 1;

  // The approximate geometric center of the geofence area.  It is calculated
  // exclusively based on the geometric shape of the geofence area and in no
  // way indicates the mobile device's actual location within the geofence
  // area. If multiple hyperlocal polygons are specified above then
  // center_point is the geometric center of all hyperlocal polygons.
  optional Hyperlocal.Point center_point = 2;
}

// Hyperlocal targeting signal when available, encrypted as described at
// https://developers.google.com/authorized-buyers/rtb/response-guide/decrypt-hyperlocal
optional bytes encrypted_hyperlocal_set = 40;

每个比邻商家定位信号都包含一个或多个多边形和一个中心点。对于每个多边形,比邻商家定位信号都包含:

  • 多边形的每个角的纬度和经度依次作为重复的 corners 字段传递。
  • 地理围栏区域的大致几何中心,在可选的 center_point 字段中传递。

定位信号的结构

BidRequest.encrypted_hyperlocal_set 中包含的加密比邻商家定位信号包含 3 个部分:

  • initialization_vector:16 字节。
  • ciphertext:一系列 20 字节的部分。
  • integrity_signature:4 字节。
{initialization_vector (16 bytes)}{ciphertext (20-byte sections)}{integrity_signature (4 bytes)}

ciphertext 字节数组分为多个 20 字节部分,但最后一部分可能包含 1 到 20 个字节(含 1 和 20 字节)。对于原始 byte_array 的每个部分,系统会生成相应的 20 字节 ciphertext,如下所示:

<byte_array <xor> HMAC(encryption_key, initialization_vector || counter_bytes)>

其中 || 是串联的。

定义

变量 详细信息
initialization_vector 16 个字节 - 展示专用。
encryption_key 32 个字节 - 在创建帐号时提供。
integrity_key 32 个字节 - 在创建帐号时提供。
byte_array 20 字节部分的序列化 HyperlocalSet 对象。
counter_bytes 显示区序号的字节值(见下文)。
final_message 通过 BidRequest.encrypted_hyperlocal_set 字段发送的字节数组。
运算符 详细信息
hmac(key, data) SHA-1 HMAC,使用 key 加密 data
a || b 字符串 a 与字符串 b 串联。

计算计数器字节数

counter_bytes 表示 ciphertext 中每个 20 字节部分的顺序。请注意,最后一部分可能包含 1-20(含)字节。如需在运行 hmac() 函数时用正确的值填充 counter_bytes,请计算 20 字节部分(包括剩余部分)并使用以下引用表:

编号 counter_bytes
0
1 ... 256 个 1 个字节。值从 0 递增到 255。
257 ... 512 2 个字节。第一个字节的值为 0,第二个字节的值从 0 依次递增到 255。
513 ... 768 3 个字节。前两个字节的值为 0,最后一个字节的值依序从 0 到 255。

我们预计 BidRequest.encrypted_hyperlocal_set 的长度不会超过 1 KB,甚至还会进一步考虑增长。不过,counter_bytes 可以任意长,以支持任意长度的比邻商家定位信号。

加密方案

比邻商家定位信号的加密方案基于与解密价格确认相同的方案。

  1. 序列化:比邻商家定位信号是 proto 中定义的 HyperlocalSet 对象的实例,首先通过 SerializeAsString() 序列化为字节数组。

  2. 加密:随后,该字节数组使用自定义加密方案进行加密,该方案可最大限度地减小大小开销,同时确保足够的安全性。该加密方案使用基于密钥的 HMAC 算法,根据展示事件所特有的 initialization_vector 生成密码块。

加密伪代码

byte_array = SerializeAsString(HyperlocalSet object)
pad = hmac(encryption_key, initialization_vector || counter_bytes )  // for each 20-byte section of byte_array
ciphertext = pad <xor> byte_array // for each 20-byte section of byte_array
integrity_signature = hmac(integrity_key, byte_array || initialization_vector)  // first 4 bytes
final_message = initialization_vector || ciphertext || integrity_signature

解密方案

您的解密代码必须 1) 使用加密密钥解密比邻商家定位信号,以及 2) 使用完整性密钥验证完整性位。系统会在帐号开设过程中向您提供密钥。您的实现结构没有任何限制。在大多数情况下,您应该能够使用示例代码,并根据您的需要对其进行调整。

  1. 生成 padHMAC(encryption_key, initialization_vector || counter_bytes)
  2. XOR:将此结果与密文一起<xor>进行逆转。
  3. 验证:完整性签名传递 4 个字节的 HMAC(integrity_key, byte_array || initialization_vector)

解密伪代码

(initialization_vector, ciphertext, integrity_signature) = final_message // split up according to length rules
pad = hmac(encryption_key, initialization_vector || counter_bytes)  // for each 20-byte section of ciphertext
byte_array = ciphertext <xor> pad // for each 20-byte section of ciphertext
confirmation_signature = hmac(integrity_key, byte_array || initialization_vector)
success = (confirmation_signature == integrity_signature)

C++ 代码示例

完整的解密示例代码中包含一个关键函数。

bool DecryptByteArray(
    const string& ciphertext, const string& encryption_key,
    const string& integrity_key, string* cleartext) {
  // Step 1. find the length of initialization vector and clear text.
  const int cleartext_length =
      ciphertext.size() - kInitializationVectorSize - kSignatureSize;
  if (cleartext_length < 0) {
    // The length cannot be correct.
    return false;
  }

  string iv(ciphertext, 0, kInitializationVectorSize);

  // Step 2. recover clear text
  cleartext->resize(cleartext_length, '\0');
  const char* ciphertext_begin = string_as_array(ciphertext) + iv.size();
  const char* const ciphertext_end = ciphertext_begin + cleartext->size();
  string::iterator cleartext_begin = cleartext->begin();

  bool add_iv_counter_byte = true;
  while (ciphertext_begin < ciphertext_end) {
    uint32 pad_size = kHashOutputSize;
    uchar encryption_pad[kHashOutputSize];

    if (!HMAC(EVP_sha1(), string_as_array(encryption_key),
              encryption_key.length(), (uchar*)string_as_array(iv),
              iv.size(), encryption_pad, &pad_size)) {
      printf("Error: encryption HMAC failed.\n");
      return false;
    }

    for (int i = 0;
         i < kBlockSize && ciphertext_begin < ciphertext_end;
         ++i, ++cleartext_begin, ++ciphertext_begin) {
      *cleartext_begin = *ciphertext_begin ^ encryption_pad[i];
    }

    if (!add_iv_counter_byte) {
      char& last_byte = *iv.rbegin();
      ++last_byte;
      if (last_byte == '\0') {
        add_iv_counter_byte = true;
      }
    }

    if (add_iv_counter_byte) {
      add_iv_counter_byte = false;
      iv.push_back('\0');
    }
  }
}

比邻商家信号和按键示例

如需测试和验证您的代码,请执行以下操作:

  1. 将包含 308 个十六进制字符的字符串转换为由 154 个字节组成的数组。例如,假设存在以下字符串:
    E2014EA201246E6F6E636520736F7572636501414243C0ADF6B9B6AC17DA218FB50331EDB376701309CAAA01246E6F6E636520736F7572636501414243C09ED4ECF2DB7143A9341FDEFD125D96844E25C3C202466E6F6E636520736F7572636502414243517C16BAFADCFAB841DE3A8C617B2F20A1FB7F9EA3A3600256D68151C093C793B0116DB3D0B8BE9709304134EC9235A026844F276797
    
    将其转换为 154 字节的数组,如下所示:
    const char serialized_result[154] = { 0xE2, 0x01, 0x4E, ... };
    
  2. 调用 BidRequest.ParsePartialFromString() 方法,将 154 字节的数组反序列化为 BidRequest 协议缓冲区。
    BidRequest bid_req;
    bid_req.ParsePartialFromString(serialzed_result);
    
  3. 验证 BidRequest 是否只包含 3 个字段:
    • encrypted_hyperlocal_set
      BidReqeust 消息中声明。
    • encrypted_advertising_id
      BidReqeust.Mobile 消息中声明。
    • encrypted_hashed_idfa
      BidReqeust.Mobile 消息中声明。

    例如:

    encrypted_hyperlocal_set:(
        {  100,  100 },
        {  200, -300 },
        { -400,  500 },
        { -600, -700 },)
    encrypted_advertising_id: { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 }
    encrypted_hashed_idfa : { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0xF1 }
    
  4. 使用以下 encryption_keyintegrity_key 解密这 3 个字段,并验证它们是否正确解密。
    encryption_key = {0x02, 0xEE, 0xa8, 0x3c, 0x6c, 0x12, 0x11, 0xe1, 0x0b,
        0x9f, 0x88, 0x96, 0x6c, 0xee, 0xc3, 0x49, 0x08, 0xeb, 0x94, 0x6f, 0x7e,
        0xd6, 0xe4, 0x41, 0xaf, 0x42, 0xb3, 0xc0, 0xf3, 0x21, 0x81, 0x40};
    
    integrity_key = {0xbf, 0xFF, 0xec, 0x55, 0xc3, 0x01, 0x30, 0xc1, 0xd8,
        0xcd, 0x18, 0x62, 0xed, 0x2a, 0x4c, 0xd2, 0xc7, 0x6a, 0xc3, 0x3b, 0xc0,
        0xc4, 0xce, 0x8a, 0x3d, 0x3b, 0xbd, 0x3a, 0xd5, 0x68, 0x77, 0x92};
    

检测过时响应攻击

为了检测过时的响应攻击,我们建议在考虑时区差异之后,使用与系统时间明显不同的时间戳过滤响应。我们的服务器已设置为 PST/PDT 时间。

如需了解实现详情,请参阅解密价格确认一文中的“检测过时响应攻击”。

Java 库

您可以使用 DoubleClickCrypto.java,而无需实施加密算法对比邻商家定位信号进行编码和解码。如需了解详情,请参阅加密