Déchiffrement des identifiants d'annonceur pour les réseaux publicitaires

Les réseaux publicitaires qui utilisent des tags JavaScript pour remplir les annonces via Authorized Buyers peuvent recevoir des identifiants d'annonceurs pour les appareils Android et iOS. Ces informations sont envoyées via la macro %%EXTRA_TAG_DATA%% ou %%ADVERTISING_IDENTIFIER%% dans la balise JavaScript gérée par Authorized Buyers. Le reste de cette section se concentre sur l'extraction de %%EXTRA_TAG_DATA%%, mais consultez la section Remarketing avec l'IDFA ou l'identifiant publicitaire pour en savoir plus sur le %%ADVERTISING_IDENTIFIER%% tampon proto chiffré MobileAdvertisingId, qui peut être déchiffré de manière similaire.

Chronologie

  1. Le réseau publicitaire met à jour ses tags JavaScript intégrés à l'application via l'interface utilisateur Authorized Buyers, en ajoutant la macro %%EXTRA_TAG_DATA%%, comme expliqué ci-dessous.
  2. Au moment de la diffusion, l'application demande une annonce à Authorized Buyers via le SDK Google Mobile Ads, tout en transmettant l'identifiant de l'annonceur de manière sécurisée.
  3. L'application reçoit en retour le tag JavaScript, avec la macro %%EXTRA_TAG_DATA%% renseignée avec le tampon de protocole de réseau publicitaire chiffré contenant cet identifiant.
  4. L'application exécute ce tag et appelle le réseau publicitaire pour obtenir l'annonce gagnante.
  5. Pour utiliser (monétiser) ces informations, le réseau publicitaire doit traiter le tampon de protocole :
    1. Décodez la chaîne Web sécurisée en une chaîne d'octets avec WebSafeBase64.
    2. Déchiffrez-le en suivant le schéma ci-dessous.
    3. Désérialisez le proto et obtenez la référence annonceur à partir de ExtraTagData.advertising_id ou ExtraTagData.hashed_idfa.

Dépendances

  1. L'encodeur WebSafeBase64.
  2. Une bibliothèque de chiffrement compatible avec SHA-1 HMAC, telle que Openssl.
  3. Le compilateur de tampon de protocole Google.

Décoder une chaîne Web sécurisée

Étant donné que les informations envoyées via la macro %%EXTRA_TAG_DATA%% doivent être envoyées via une URL, les serveurs Google les encodent au format base64 adapté au Web (RFC 3548).

Par conséquent, avant d'essayer de déchiffrement, vous devez décoder les caractères ASCII en une chaîne d'octets. L'exemple de code C++ ci-dessous est basé sur la fonction BIO_f_base64() du projet OpenSSL et fait partie de l'exemple de code de déchiffrement de 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);
}

Structure d'une chaîne d'octets chiffrée

Une fois que vous avez décodé les caractères ASCII en une chaîne d'octets, vous êtes prêt à la déchiffrer. La chaîne d'octets chiffrée contient trois sections:

  • initialization_vector: 16 octets.
  • ciphertext: série de sections de 20 octets.
  • integrity_signature: 4 octets.
{initialization_vector (16 bytes)}{ciphertext (20-byte sections)}{integrity_signature (4 bytes)}

Le tableau d'octets ciphertext est divisé en plusieurs sections de 20 octets, à l'exception du fait que la toute dernière section peut contenir entre 1 et 20 octets inclus. Pour chaque section du fichier byte_array d'origine, le ciphertext de 20 octets correspondant est généré comme suit:

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

|| correspond à la concaténation.

Définitions

Variable Détails
initialization_vector 16 octets : propre à l'impression.
encryption_key 32 octets : fournis lors de la configuration du compte.
integrity_key 32 octets : fournis lors de la configuration du compte.
byte_array Objet ExtraTagData sérialisé, en sections de 20 octets.
counter_bytes Valeur en octets indiquant le numéro ordinal de la section, voir ci-dessous.
final_message Tableau d'octets total envoyé via la macro %%EXTRA_TAG_DATA%% (moins l'encodage WebSafeBase64).
Opérateurs Détails
hmac(key, data) HMAC SHA-1, utilisant key pour chiffrer data.
a || b la chaîne a concaténée avec la chaîne b.

Calculer compteur_octets

counter_bytes marque l'ordre de chaque section de 20 octets de ciphertext. Notez que la dernière section peut contenir entre 1 et 20 octets inclus. Pour renseigner counter_bytes avec la valeur correcte lors de l'exécution de votre fonction hmac(), comptez les sections de 20 octets (y compris le reste) et utilisez le tableau de référence suivant:

Numéro de section Valeur counter_bytes
0 Aucun
1 ... 256 1 octet. La valeur est incrémentée de 0 à 255 de manière séquentielle.
257 ... 512 2 octets. La valeur du premier octet est 0, la valeur du deuxième octet passe de 0 à 255 de manière séquentielle.
513 ... 768 3 octets. La valeur des deux premiers octets est égale à 0, et la valeur du dernier octet passe de 0 à 255 de manière séquentielle.

Haut de page

Schéma de chiffrement

Le schéma de chiffrement est basé sur le même schéma que celui utilisé pour déchiffrer le signal de ciblage hyperlocalisé.

  1. Sérialisation: une instance de l'objet ExtraTagData, tel que défini dans le tampon de protocole, est d'abord sérialisée via SerializeAsString() dans un tableau d'octets.

  2. Chiffrement: le tableau d'octets est ensuite chiffré à l'aide d'un schéma de chiffrement personnalisé conçu pour minimiser la surcharge de la taille tout en assurant une sécurité adéquate. Le schéma de chiffrement utilise un algorithme HMAC à clé pour générer un bloc secret basé sur le initialization_vector, qui est propre à l'événement d'impression.

Pseudo-code de chiffrement

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

Schéma de déchiffrement

Votre code de déchiffrement doit 1) déchiffrer le tampon de protocole à l'aide de la clé de chiffrement et 2) vérifier les bits d'intégrité à l'aide de la clé d'intégrité. Les clés vous seront fournies lors de la configuration du compte. Il n'existe aucune restriction concernant la structure de votre implémentation. Dans la plupart des cas, vous devriez pouvoir utiliser l'exemple de code et l'adapter à vos besoins.

  1. Générez votre pad: HMAC(encryption_key, initialization_vector || counter_bytes)
  2. XOR: prenez ce résultat et <xor> avec le texte chiffré pour inverser le chiffrement.
  3. Vérifier: la signature d'intégrité transmet 4 octets de HMAC(integrity_key, byte_array || initialization_vector).

Pseudo-code de déchiffrement

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

Exemple de code C++

Vous trouverez ci-dessous une fonction clé tirée de notre exemple de code de déchiffrement complet.

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

Obtenir les données du tampon de protocole de réseau publicitaire

Une fois que vous avez décodé et déchiffré les données transmises dans %%EXTRA_TAG_DATA%%, vous pouvez désérialiser le tampon de protocole et obtenir l'identifiant de l'annonceur pour le ciblage.

Si vous ne connaissez pas les tampons de protocole, consultez notre documentation.

Définition

Notre tampon de protocole de réseau publicitaire est défini comme suit:

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

Vous devez la désérialiser à l'aide de ParseFromString(), comme décrit dans la documentation sur le tampon de protocole C++.

Pour en savoir plus sur les champs Android advertising_id et iOS hashed_idfa, consultez Déchiffrer l'identifiant publicitaire et Cibler l'inventaire d'applications mobiles avec l'IDFA.

Bibliothèque Java

Au lieu de mettre en œuvre les algorithmes de chiffrement pour encoder et décoder les identifiants d'annonceur pour les réseaux publicitaires, vous pouvez utiliser DoubleClickCrypto.java. Pour en savoir plus, consultez la page Cryptographie.