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
}