解密广告网络的广告客户标识符

使用 JavaScript 代码通过 Authorized Buyers 填充广告的广告联盟可以接收 Android 和 iOS 设备的广告客户标识符。这些信息通过 Authorized Buyers 管理的 JavaScript 代码中的 %%EXTRA_TAG_DATA%%%%ADVERTISING_IDENTIFIER%% 宏发送。本部分的其余内容重点介绍如何提取 %%EXTRA_TAG_DATA%%;不过,如需详细了解可进行类似解密的 %%ADVERTISING_IDENTIFIER%% 加密 proto 缓冲区 MobileAdvertisingId,请参阅 利用 IDFA 或广告 ID 进行再营销

时间表

  1. 广告联盟会通过 Authorized Buyers 界面更新其 JavaScript 应用内代码,并添加 %%EXTRA_TAG_DATA%% 宏(如下所述)。
  2. 在投放时,应用通过 Google 移动广告 SDK 向 Authorized Buyers 请求广告,同时安全地传递广告客户标识符。
  3. 应用收到 JavaScript 代码,并在 %%EXTRA_TAG_DATA%% 宏中填充包含该标识符的加密广告联盟协议缓冲区。
  4. 应用运行此代码,向广告联盟发出胜出的广告调用。
  5. 为了使用这些信息(用于创收),广告联盟必须处理协议缓冲区:
    1. 使用 WebSafeBase64 将网络安全字符串解码回字节串。
    2. 按照下述方案解密。
    3. 对该 proto 进行反序列化,并从 ExtraTagData.advertising_id 或 ExtraTagData.hashed_idfa 获取广告客户 ID。

依赖项

  1. WebSafeBase64 编码器
  2. 支持 SHA-1 HMAC 的加密库,例如 Openssl
  3. Google 协议缓冲区编译器

解码网络安全字符串

由于通过 %%EXTRA_TAG_DATA%% 宏发送的信息必须通过网址发送,因此 Google 服务器会使用可在 web 环境中安全使用的 base64 (RFC 3548) 对其进行编码。

因此,在尝试解密之前,您必须将 ASCII 字符重新解码为字节字符串。以下示例 C++ 代码基于 OpenSSL 项目的 BIO_f_base64(),是 Google 示例解密代码的一部分。

string AddPadding(const string& b64_string) {
  if (b64_string.size() % 4 == 3) {
    return b64_string + "=";
  } else if (b64_string.size() % 4 == 2) {
    return b64_string + "==";
  }
  return b64_string;
}

// Adapted from http://www.openssl.org/docs/man1.1.0/crypto/BIO_f_base64.html
// Takes a web safe base64 encoded string (RFC 3548) and decodes it.
// Normally, web safe base64 strings have padding '=' replaced with '.',
// but we will not pad the ciphertext. We add padding here because
// openssl has trouble with unpadded strings.
string B64Decode(const string& encoded) {
  string padded = AddPadding(encoded);
  // convert from web safe -> normal base64.
  int32 index = -1;
  while ((index = padded.find_first_of('-', index + 1)) != string::npos) {
    padded[index] = '+';
  }
  index = -1;
  while ((index = padded.find_first_of('_', index + 1)) != string::npos) {
    padded[index] = '/';
  }

  // base64 decode using openssl library.
  const int32 kOutputBufferSize = 256;
  char output[kOutputBufferSize];

  BIO* b64 = BIO_new(BIO_f_base64());
  BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
  BIO* bio = BIO_new_mem_buf(const_cast(padded.data()),
                             padded.length());
  bio = BIO_push(b64, bio);
  int32 out_length = BIO_read(bio, output, kOutputBufferSize);
  BIO_free_all(bio);
  return string(output, out_length);
}

加密字节串的结构

将 ASCII 字符解码回字节串后,即可对其进行解密。加密的字节串包含 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 序列化 ExtraTagData 对象,分为 20 字节。
counter_bytes 显示该区段序数的字节值,如下所示。
final_message 通过 %%EXTRA_TAG_DATA%% 宏发送的总字节数组(减 WebSafeBase64 编码)。
运算符 具体说明
hmac(key, data) SHA-1 HMAC,使用 key 加密 data
a || b 字符串 a 与字符串 b 串联。

计算计数器字节数

counter_bytes 标记 ciphertext 中每个 20 字节部分的顺序。请注意,最后一部分可能包含 1-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。

返回页首

加密架构

该加密架构与解密超局部定位信号时所用的架构相同。

  1. 序列化:协议缓冲区中定义的 ExtraTagData 对象的实例首先通过 SerializeAsString() 序列化为字节数组。

  2. 加密:字节数组随后使用自定义加密架构进行加密,该架构旨在最大限度地减少大小开销,同时确保充分的安全性。该加密方案使用密钥加密的 HMAC 算法,根据展示事件所独有的 initialization_vector 生成密钥填充内容。

加密伪代码

byte_array = SerializeAsString(ExtraTagData 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. 生成平铺HMAC(encryption_key, initialization_vector || counter_bytes)
  2. XOR:将此结果与<xor>连同密文以逆转加密。
  3. 验证:完整性签名传递 4 个字节的 HMAC(integrity_key, byte_array || initialization_vector)

解密伪代码

// split up according to length rules
(initialization_vector, ciphertext, integrity_signature) = final_message

// for each 20-byte section of ciphertext
pad = hmac(encryption_key, initialization_vector || counter_bytes)

// for each 20-byte section of ciphertext
byte_array = ciphertext <xor> pad

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

从广告网络协议缓冲区获取数据

%%EXTRA_TAG_DATA%% 中传递的数据进行解码和解密后,您便可以对协议缓冲区进行反序列化,并获取用于定位的广告客户标识符。

如果您不熟悉协议缓冲区,请先参阅我们的文档

定义

我们的广告网络协议缓冲区定义如下:

message ExtraTagData {
  // advertising_id can be Apple's identifier for advertising (IDFA)
  // or Android's advertising identifier. When the advertising_id is an IDFA,
  // it is the plaintext returned by iOS's [ASIdentifierManager
  // advertisingIdentifier]. For hashed_idfa, the plaintext is the MD5 hash of
  // the IDFA.  Only one of the two fields will be available, depending on the
  // version of the SDK making the request.  Later SDKs provide unhashed values.
  optional bytes advertising_id = 1;
  optional bytes hashed_idfa = 2;
}

您需要使用 ParseFromString() 对其进行反序列化,如 C++ 协议缓冲区文档中所述。

如需详细了解 Android advertising_id 和 iOS hashed_idfa 字段,请参阅解密广告 ID使用 IDFA 定位到移动应用内广告资源

Java 库

您可以使用 DoubleClickCrypto.java,而不是实现加密算法来对广告网络的广告客户标识符进行编码和解码。如需了解详情,请参阅加密