01 什么是DoubleClick

广告赢价后将赢价价格告知广告主,此时为数据安全和保密性需要进行加密。Google AD采用的是其自定义的加解密算法,即DoubleClick算法。

DoubleClick算法需要两个密钥,分别为完整性密钥(integrity key)和加密密钥(encryption key),两个key交互双方共享,两者是以安全的base64URL编码后的值。如:

skU7Ax_NL5pPAFyKdkfZjZz2-VhIN8bjj1rVFOaJ_5o=  // Encryption key (e_key)
arO23ykdNqUQ5LEoQ0FVmPkBd7xB5CO89PDZlSjpFxo=  // Integrity key (i_key)

在进行加解密时则需要将其从SafeURLBase64中Decode出来

e_key = WebSafeBase64Decode('skU7Ax_NL5pPAFyKdkfZjZz2-VhIN8bjj1rVFOaJ_5o=')
i_key = WebSafeBase64Decode('arO23ykdNqUQ5LEoQ0FVmPkBd7xB5CO89PDZlSjpFxo=')

02 加密步骤

pad = hmac(e_key, iv) enc_price = pad ^ pricesignature = hmac(i_key, price || iv)final_message = WebSafeBase64Encode(iv || enc_price || signatrue)

即最终的加密字符串组成格式为:

iv(16字节) encrypted_price (8字节) integrity(4字节)

在解密时需要按照该格式从字符串中得到各个部分。

03 解密步骤

=final_message_valid_base64 = AddBase64Padding(final_message)enc_price = WebSafeBase64Decode(final_message_valid_base64)(iv, p, sig) = enc_priceprice_pad = hmac(e_key, iv) price = p xor price_padconf_sig =hmac(i_key, price || iv) success = (conf_sig == sig)

04 Reference

05 Golang版本实现

Google官方只有Java和C++实现,补充了以下Golang版本的实现。

package util

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"time"
)

type GoogleDoubleClickTuil struct {
	encryptionKey []byte
	integrityKey  []byte
	encoder       *base64.Encoding
}

func NewGoogleDoubleClickTuil(iKey, eKey string) (*GoogleDoubleClickTuil, error) {
	u := &GoogleDoubleClickTuil{}
	u.encoder = base64.URLEncoding

	var err error
	if u.encryptionKey, err = u.genKey([]byte(u.encoder.EncodeToString([]byte(eKey)))); err != nil {
		return nil, err
	}

	if u.integrityKey, err = u.genKey([]byte(u.encoder.EncodeToString([]byte(iKey)))); err != nil {
		return nil, err
	}

	return u, nil
}

func (s *GoogleDoubleClickTuil) Encrypt(price uint64) (string, error) {
	iv := []byte(fmt.Sprintf("%d", time.Now().UnixNano()/1000))

	if len(s.integrityKey) == 0 || len(s.encryptionKey) == 0 {
		return "", errors.New("bad integrityKey or encryptionKey")
	}

	if len(iv) != 16 {
		return "", errors.New("bad iv")
	}

	h := hmac.New(sha1.New, s.encryptionKey)
	h.Write(iv)
	pad := h.Sum(nil)[:8]

	p := make([]byte, 8)
	binary.BigEndian.PutUint64(p, price)
	encPrice := s.safeXORBytes(pad, p)

	h = hmac.New(sha1.New, s.integrityKey)
	h.Write(p)
	h.Write(iv)
	sig := h.Sum(nil)[:4]

	b := make([]byte, 0, len(iv)+len(encPrice)+len(sig))
	buf := bytes.NewBuffer(b)
	buf.Write(iv)
	buf.Write(encPrice)
	buf.Write(sig)
	n := base64.RawURLEncoding.EncodedLen(len(buf.Bytes()))
	ret := make([]byte, n, n)
	base64.RawURLEncoding.Encode(ret, buf.Bytes())

	return string(ret), nil
}

func (s *GoogleDoubleClickTuil) Decriypt(encPriceStr string) (uint64, error) {
	if len(s.integrityKey) == 0 || len(s.encryptionKey) == 0 {
		return 0, errors.New("encryption and integrity keys are required")
	}

	encPrice := []byte(encPriceStr)

	if len(encPrice) != 38 {
		return 0, fmt.Errorf("invalid price: invalid length, expected 38 got %d", len(encPrice))
	}

	dprice := make([]byte, base64.RawURLEncoding.DecodedLen(len(encPrice)))
	n, err := base64.RawURLEncoding.Decode(dprice, encPrice)
	if err != nil {
		return 0, fmt.Errorf("invalid base64 string: %s", err.Error())
	}
	dprice = dprice[:n]

	if len(dprice) != 28 {
		return 0, fmt.Errorf("invalid decoded price length. Expected 28 got %d", len(dprice))
	}

	iv, p, sig := dprice[0:16], dprice[16:24], dprice[24:]
	h := hmac.New(sha1.New, s.encryptionKey)
	n, err = h.Write(iv)
	if err != nil || n != len(iv) {
		return 0, fmt.Errorf("could not write hmac hash for iv. err:%s, n:%d, len(iv):%d", err, n, len(iv))
	}
	pricePad := h.Sum(nil)

	price := s.safeXORBytes(p, pricePad)
	if price == nil {
		return 0, fmt.Errorf("price xor price_pad failed")
	}

	h = hmac.New(sha1.New, s.integrityKey)
	n, err = h.Write(price)
	if err != nil || n != len(price) {
		return 0, fmt.Errorf("could not write hmac hash for price. err:%s, n:%d, len(price):%d", err, n, len(price))
	}

	n, err = h.Write(iv)
	if err != nil || n != len(iv) {
		return 0, fmt.Errorf("could not write hmac hash for iv. err:%s, n:%d, len(iv):%d", err, n, len(iv))
	}

	confSig := h.Sum(nil)[:4]
	if bytes.Compare(confSig, sig) != 0 {
		return 0, fmt.Errorf("integrity of price is not valid")
	}

	return binary.BigEndian.Uint64(price), nil
}

func (s *GoogleDoubleClickTuil) genKey(target []byte) ([]byte, error) {
	icKey := make([]byte, s.encoder.DecodedLen(len([]byte(target))))
	n, err := s.encoder.Decode(icKey, []byte(target))
	if err != nil {
		return nil, fmt.Errorf("invalid key:%s, err: %x", target, err)
	}
	return icKey[:n], nil
}

func (s *GoogleDoubleClickTuil) safeXORBytes(a, b []byte) []byte {
	n := len(a)
	if len(b) < n {
		n = len(b)
	}

	if n == 0 {
		return nil
	}

	ret := make([]byte, n)

	for i := 0; i < n; i++ {
		ret[i] = a[i] ^ b[i]
	}

	return ret
}