All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.doubleclick.crypto.DoubleClickCrypto Maven / Gradle / Ivy

There is a newer version: 2.0.3
Show newest version
/*
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.doubleclick.crypto;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Math.min;

import com.google.common.base.MoreObjects;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.ThreadLocalRandom;
import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Encryption and decryption support for the DoubleClick Ad Exchange RTB protocol.
*
* 

Encrypted payloads are wrapped by "packages" in the general format: * * initVector:16 || E(payload:?) || I(signature:4) * *
where: *

    *
  1. {@code initVector = timestamp:8 || serverId:8} (AdX convention)
  2. *
  3. {@code E(payload) = payload ^ hmac(encryptionKey, initVector)} per max-20-byte block
  4. *
  5. {@code I(signature) = hmac(integrityKey, payload || initVector)[0..3]}
  6. *
* *

This class, and all nested classes / subclasses, are threadsafe. */ public class DoubleClickCrypto { private static final Logger logger = LoggerFactory.getLogger(DoubleClickCrypto.class); public static final String KEY_ALGORITHM = "HmacSHA1"; /** Initialization vector offset in the crypto package. */ public static final int INITV_BASE = 0; /** Initialization vector size. */ public static final int INITV_SIZE = 16; /** Timestamp subfield offset in the initialization vector. */ public static final int INITV_TIMESTAMP_OFFSET = 0; /** ServerId subfield offset in the initialization vector. */ public static final int INITV_SERVERID_OFFSET = 8; /** Payload offset in the crypto package. */ public static final int PAYLOAD_BASE = INITV_BASE + INITV_SIZE; /** Integrity signature size. */ public static final int SIGNATURE_SIZE = 4; /** Overhead (non-Payload data) total size. */ public static final int OVERHEAD_SIZE = INITV_SIZE + SIGNATURE_SIZE; private static final int COUNTER_PAGESIZE = 20; private static final int COUNTER_SECTIONS = 3 * 256 + 1; private static final int MICROS_PER_CURRENCY_UNIT = 1_000_000; private final Keys keys; /** * Initializes with the encryption keys. * * @param keys Keys for the buyer's Ad Exchange account */ public DoubleClickCrypto(Keys keys) { this.keys = keys; } /** * Decodes data, from string to binary form. * The default implementation performs websafe-base64 decoding (RFC 3548). */ @Nullable protected byte[] decode(@Nullable String data) { return data == null ? null : Base64.getUrlDecoder().decode(data); } /** * Encodes data, from binary form to string. * The default implementation performs websafe-base64 encoding (RFC 3548). */ @Nullable protected String encode(@Nullable byte[] data) { return data == null ? null : Base64.getUrlEncoder().encodeToString(data); } /** * Decrypts data. * * @param cipherData {@code initVector || E(payload) || I(signature)} * @return {@code initVector || payload || I'(signature)} * Where I'(signature) == I(signature) for success, different for failure */ public byte[] decrypt(byte[] cipherData) throws SignatureException { checkArgument(cipherData.length >= OVERHEAD_SIZE, "Invalid cipherData, %s bytes", cipherData.length); // workBytes := initVector || E(payload) || I(signature) byte[] workBytes = cipherData.clone(); ByteBuffer workBuffer = ByteBuffer.wrap(workBytes); boolean success = false; try { // workBytes := initVector || payload || I(signature) xorPayloadToHmacPad(workBytes); // workBytes := initVector || payload || I'(signature) int confirmationSignature = hmacSignature(workBytes); int integritySignature = workBuffer.getInt(workBytes.length - SIGNATURE_SIZE); workBuffer.putInt(workBytes.length - SIGNATURE_SIZE, confirmationSignature); if (confirmationSignature != integritySignature) { throw new SignatureException("Signature mismatch: " + Integer.toHexString(confirmationSignature) + " vs " + Integer.toHexString(integritySignature)); } if (logger.isDebugEnabled()) { logger.debug(dump("Decrypted", cipherData, workBytes)); } success = true; return workBytes; } finally { if (!success && logger.isDebugEnabled()) { logger.debug(dump("Decrypted (failed)", cipherData, workBytes)); } } } /** * Encrypts data. * * @param plainData {@code initVector || payload || zeros:4} * @return {@code initVector || E(payload) || I(signature)} */ public byte[] encrypt(byte[] plainData) { checkArgument(plainData.length >= OVERHEAD_SIZE, "Invalid plainData, %s bytes", plainData.length); // workBytes := initVector || payload || zeros:4 byte[] workBytes = plainData.clone(); ByteBuffer workBuffer = ByteBuffer.wrap(workBytes); boolean success = false; try { // workBytes := initVector || payload || I(signature) int signature = hmacSignature(workBytes); workBuffer.putInt(workBytes.length - SIGNATURE_SIZE, signature); // workBytes := initVector || E(payload) || I(signature) xorPayloadToHmacPad(workBytes); if (logger.isDebugEnabled()) { logger.debug(dump("Encrypted", plainData, workBytes)); } success = true; return workBytes; } finally { if (!success && logger.isDebugEnabled()) { logger.debug(dump("Encrypted (failed)", plainData, workBytes)); } } } /** * Creates the initialization vector from component {@code (timestamp, serverId)} fields. * * @param timestamp Timestamp subfield. Notice that Data is not ideal for this because it's * limited to millisecond precision, which leaves leave some bits unused in the init vector * @param serverId Server ID subfield (whatever a server uses as a public ID, e.g. its IPv4) * @return initialization vector * @see #createInitVector(long, long) */ public byte[] createInitVector(@Nullable Date timestamp, long serverId) { return createInitVector( timestamp == null ? 0L : millisToSecsAndMicros(timestamp.getTime()), serverId); } /** * Creates the initialization vector from component {@code (timestamp, serverId)} fields. * This is the format used by DoubleClick, and it's a good format generally, * even though the initialization vector can be any random data (a cryptographic nonce). * *

NOTE: Follow the advice from * https://developers.google.com/ad-exchange/rtb/response-guide/decrypt-price#detecting_stale * by using a high-resolution timestamp; also if the {@code serverId} is not necessary, providing * a random value there helps further to prevent replay attacks. In all methods that have * a {@code initVector} parameter, passing null will cause {@code (current time, random)} * to be used (so if you really want all-zeros {@code initVector}, e.g. in unit tests to make * results reproducible, pass a zero-filled array). * * @param timestamp Timestamp subfield. Notice this is not supposed to be a millis/nanos value * like in common Java API; it should be: seconds-since-epoch in the upper 32bits, * microseconds in the lower 32 bits * @param serverId Server ID subfield (whatever a server uses as a public ID, e.g. its IPv4) * @return initialization vector */ public byte[] createInitVector(long timestamp, long serverId) { byte[] initVector = new byte[INITV_SIZE]; ByteBuffer byteBuffer = ByteBuffer.wrap(initVector); byteBuffer.putLong(INITV_TIMESTAMP_OFFSET, timestamp); byteBuffer.putLong(INITV_SERVERID_OFFSET, serverId); return initVector; } /** * Returns the {@code timestamp} field from encrypted or decrypted data. Assumes that its * initialization vector has the structure {@code (timestamp, serverId)}. * * @param data Encrypted or decrypted data (the initialization vector is never encrypted) * @return Timestamp subfield of the initialization vector, in the form of a Date. * This assumes the init vector was created with {@link #createInitVector(Date, long)} * or similar method consistent with the DoubleClick crypto specification */ public Date getTimestamp(byte[] data) { long secondsAndMicros = ByteBuffer.wrap(data).getLong(INITV_BASE + INITV_TIMESTAMP_OFFSET); return new Date(secsAndMicrosToMillis(secondsAndMicros)); } /** * Returns the {@code serverId} field from encrypted or decrypted data. Assumes that its * initialization vector has the structure {@code (timestamp, serverId)}. * * @param data Encrypted or decrypted data (the initialization vector is never encrypted) * @return Timestamp subfield of the initialization vector. */ public long getServerId(byte[] data) { return ByteBuffer.wrap(data).getLong(INITV_BASE + INITV_SERVERID_OFFSET); } /** * Packages plaintext payload for encryption; returns {@code initVector || payload || zeros:4}. */ protected byte[] initPlainData(int payloadSize, @Nullable byte[] initVector) { byte[] plainData = new byte[OVERHEAD_SIZE + payloadSize]; if (initVector == null) { ByteBuffer byteBuffer = ByteBuffer.wrap(plainData); byteBuffer.putLong(INITV_TIMESTAMP_OFFSET, millisToSecsAndMicros(System.currentTimeMillis())); byteBuffer.putLong(INITV_SERVERID_OFFSET, ThreadLocalRandom.current().nextLong()); } else { System.arraycopy(initVector, 0, plainData, INITV_BASE, min(INITV_SIZE, initVector.length)); } return plainData; } /** * {@code payload = payload ^ hmac(encryptionKey, initVector || counterBytes)} * per max-20-byte blocks. */ private void xorPayloadToHmacPad(byte[] workBytes) { int payloadSize = workBytes.length - OVERHEAD_SIZE; int sections = (payloadSize + COUNTER_PAGESIZE - 1) / COUNTER_PAGESIZE; checkArgument(sections <= COUNTER_SECTIONS, "Payload is %s bytes, exceeds limit of %s", payloadSize, COUNTER_PAGESIZE * COUNTER_SECTIONS); Mac encryptionHmac = createMac(); byte[] pad = new byte[COUNTER_PAGESIZE + 3]; int counterSize = 0; for (int section = 0; section < sections; ++section) { int sectionBase = section * COUNTER_PAGESIZE; int sectionSize = min(payloadSize - sectionBase, COUNTER_PAGESIZE); try { encryptionHmac.reset(); encryptionHmac.init(keys.getEncryptionKey()); encryptionHmac.update(workBytes, INITV_BASE, INITV_SIZE); if (counterSize != 0) { encryptionHmac.update(pad, COUNTER_PAGESIZE, counterSize); } encryptionHmac.doFinal(pad, 0); } catch (ShortBufferException | InvalidKeyException e) { throw new IllegalStateException(e); } for (int i = 0; i < sectionSize; ++i) { workBytes[PAYLOAD_BASE + sectionBase + i] ^= pad[i]; } Arrays.fill(pad, 0, COUNTER_PAGESIZE, (byte) 0); if (counterSize == 0 || ++pad[COUNTER_PAGESIZE + counterSize - 1] == 0) { ++counterSize; } } } private static Mac createMac() { try { return Mac.getInstance("HmacSHA1"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } } /** * {@code signature = hmac(integrityKey, payload || initVector)} */ private int hmacSignature(byte[] workBytes) { try { Mac integrityHmac = createMac(); integrityHmac.init(keys.getIntegrityKey()); integrityHmac.update(workBytes, PAYLOAD_BASE, workBytes.length - OVERHEAD_SIZE); integrityHmac.update(workBytes, INITV_BASE, INITV_SIZE); return Ints.fromByteArray(integrityHmac.doFinal()); } catch (InvalidKeyException e) { throw new IllegalStateException(e); } } private static String dump(String header, byte[] inData, byte[] workBytes) { ByteBuffer initvBuffer = ByteBuffer.wrap(workBytes, INITV_BASE, INITV_SIZE); Date timestamp = new Date(initvBuffer.getLong(INITV_BASE + INITV_TIMESTAMP_OFFSET)); long serverId = initvBuffer.getLong(INITV_BASE + INITV_SERVERID_OFFSET); return new StringBuilder() .append(header) .append(": initVector={timestamp ") .append(DateFormat.getDateTimeInstance().format(timestamp)) .append(", serverId ").append(serverId) .append("}, input =").append(BaseEncoding.base16().encode(inData)) .append(", output =").append(BaseEncoding.base16().encode(workBytes)) .toString(); } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues() .add("keys", keys) .toString(); } private static long millisToSecsAndMicros(long timestamp) { return ((timestamp / 1000) << 32) | ((timestamp % 1000) * 1000); } private static long secsAndMicrosToMillis(long secondsAndMicros) { return ((secondsAndMicros >> 32) * 1000) + (secondsAndMicros & 0xFFFFFFFFL) / 1000; } /** * Holds the keys used to configure DoubleClick cryptography. */ public static class Keys { private final SecretKey encryptionKey; private final SecretKey integrityKey; public Keys(SecretKey encryptionKey, SecretKey integrityKey) throws InvalidKeyException { this.encryptionKey = encryptionKey; this.integrityKey = integrityKey; // Forces early failure if any of the keys are not good. // This allows us to spare callers from InvalidKeyException in several methods. Mac hmac = DoubleClickCrypto.createMac(); hmac.init(encryptionKey); hmac.reset(); hmac.init(integrityKey); hmac.reset(); } public SecretKey getEncryptionKey() { return encryptionKey; } public SecretKey getIntegrityKey() { return integrityKey; } @Override public int hashCode() { return encryptionKey.hashCode() ^ integrityKey.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } else if (!(obj instanceof Keys)) { return false; } Keys other = (Keys) obj; return encryptionKey.equals(other.encryptionKey) && integrityKey.equals(other.integrityKey); } @Override public String toString() { return MoreObjects.toStringHelper(this).omitNullValues() .add("encryptionKey", encryptionKey.getAlgorithm() + '/' + encryptionKey.getFormat()) .add("integrityKey", integrityKey.getAlgorithm() + '/' + integrityKey.getFormat()) .toString(); } } /** * Encryption for winning price. * *

See * Decrypting Price Confirmations. */ public static class Price extends DoubleClickCrypto { private static final int PAYLOAD_SIZE = 8; @Inject public Price(Keys keys) { super(keys); } /** * Encrypts the winning price. * * @param priceValue the price in micros (1/1.000.000th of the currency unit) * @param initVector up to 16 bytes of nonce data * @return encrypted price * @see #createInitVector(Date, long) */ public byte[] encryptPriceMicros(long priceValue, @Nullable byte[] initVector) { byte[] plainData = initPlainData(PAYLOAD_SIZE, initVector); ByteBuffer.wrap(plainData).putLong(PAYLOAD_BASE, priceValue); return encrypt(plainData); } /** * Decrypts the winning price. * * @param priceCipher encrypted price * @return the price value in micros (1/1.000.000th of the currency unit) */ public long decryptPriceMicros(byte[] priceCipher) throws SignatureException { checkArgument(priceCipher.length == (OVERHEAD_SIZE + PAYLOAD_SIZE), "Price is %s bytes, should be %s", priceCipher.length, (OVERHEAD_SIZE + PAYLOAD_SIZE)); byte[] plainData = decrypt(priceCipher); return ByteBuffer.wrap(plainData).getLong(PAYLOAD_BASE); } /** * Encrypts and encodes the winning price. * * @param priceMicros the price in micros (1/1.000.000th of the currency unit) * @param initVector up to 16 bytes of nonce data, or {@code null} for default * generated data (see {@link #createInitVector(Date, long)} * @return encrypted price, encoded as websafe-base64 */ public String encodePriceMicros(long priceMicros, @Nullable byte[] initVector) { return encode(encryptPriceMicros(priceMicros, initVector)); } /** * Encrypts and encodes the winning price. * * @param priceValue the price * @param initVector up to 16 bytes of nonce data, or {@code null} for default * generated data (see {@link #createInitVector(Date, long)} * @return encrypted price, encoded as websafe-base64 */ public String encodePriceValue(double priceValue, @Nullable byte[] initVector) { return encodePriceMicros((long) (priceValue * MICROS_PER_CURRENCY_UNIT), initVector); } /** * Decodes and decrypts the winning price. * * @param priceCipher encrypted price, encoded as websafe-base64 * @return the price value in micros (1/1.000.000th of the currency unit) */ public long decodePriceMicros(String priceCipher) throws SignatureException { return decryptPriceMicros(decode(checkNotNull(priceCipher))); } /** * Decodes and decrypts the winning price. * * @param priceCipher encrypted price, encoded as websafe-base64 * @return the price value */ public double decodePriceValue(String priceCipher) throws SignatureException { return decodePriceMicros(priceCipher) / ((double) MICROS_PER_CURRENCY_UNIT); } } /** * Encryption for Advertising ID. * *

See * * Decrypting Advertising ID. */ public static class AdId extends DoubleClickCrypto { private static final int PAYLOAD_SIZE = 16; @Inject public AdId(Keys keys) { super(keys); } /** * Encrypts the Advertising Id. * * @param adidPlain the AdId * @param initVector up to 16 bytes of nonce data, or {@code null} for default * generated data (see {@link #createInitVector(Date, long)} * @return encrypted AdId */ public byte[] encryptAdId(byte[] adidPlain, @Nullable byte[] initVector) { checkArgument(adidPlain.length == PAYLOAD_SIZE, "AdId is %s bytes, should be %s", adidPlain.length, PAYLOAD_SIZE); byte[] plainData = initPlainData(PAYLOAD_SIZE, initVector); System.arraycopy(adidPlain, 0, plainData, PAYLOAD_BASE, PAYLOAD_SIZE); return encrypt(plainData); } /** * Decrypts the AdId. * * @param adidCipher encrypted AdId * @return the AdId */ public byte[] decryptAdId(byte[] adidCipher) throws SignatureException { checkArgument(adidCipher.length == (OVERHEAD_SIZE + PAYLOAD_SIZE), "AdId is %s bytes, should be %s", adidCipher.length, (OVERHEAD_SIZE + PAYLOAD_SIZE)); byte[] plainData = decrypt(adidCipher); return Arrays.copyOfRange(plainData, PAYLOAD_BASE, plainData.length - SIGNATURE_SIZE); } } /** * Encryption for IDFA. * *

See * * Targeting mobile app inventory with IDFA. */ public static class Idfa extends DoubleClickCrypto { @Inject public Idfa(Keys keys) { super(keys); } /** * Encrypts the IDFA. * * @param idfaPlain the IDFA * @param initVector up to 16 bytes of nonce data, or {@code null} for default * generated data (see {@link #createInitVector(Date, long)} * @return encrypted IDFA */ public byte[] encryptIdfa(byte[] idfaPlain, @Nullable byte[] initVector) { byte[] plainData = initPlainData(idfaPlain.length, initVector); System.arraycopy(idfaPlain, 0, plainData, PAYLOAD_BASE, idfaPlain.length); return encrypt(plainData); } /** * Decrypts the IDFA. * * @param idfaCipher encrypted IDFA * @return the IDFA */ public byte[] decryptIdfa(byte[] idfaCipher) throws SignatureException { byte[] plainData = decrypt(idfaCipher); return Arrays.copyOfRange(plainData, PAYLOAD_BASE, plainData.length - SIGNATURE_SIZE); } /** * Encrypts and encodes the IDFA. * * @param idfaPlain the IDFA * @param initVector up to 16 bytes of nonce data, or {@code null} for default * generated data (see {@link #createInitVector(Date, long)} * @return encrypted IDFA, websafe-base64 encoded */ public String encodeIdfa(byte[] idfaPlain, @Nullable byte[] initVector) { return encode(encryptIdfa(idfaPlain, initVector)); } /** * Decodes and decrypts the IDFA. * * @param idfaCipher encrypted IDFA, websafe-base64 encoded * @return the IDFA */ public byte[] decodeIdfa(String idfaCipher) throws SignatureException { return decryptIdfa(decode(idfaCipher)); } } /** * Encryption for {@code HyperlocalSet} geofence information. * *

See * * Decrypting Hyperlocal Targeting Signals. */ public static class Hyperlocal extends DoubleClickCrypto { @Inject public Hyperlocal(Keys keys) { super(keys); } /** * Encrypts the serialized {@code HyperlocalSet}. * * @param hyperlocalPlain the {@code HyperlocalSet} * @param initVector up to 16 bytes of nonce data, or {@code null} for default * generated data (see {@link #createInitVector(Date, long)} * @return encrypted {@code HyperlocalSet} */ public byte[] encryptHyperlocal(byte[] hyperlocalPlain, @Nullable byte[] initVector) { byte[] plainData = initPlainData(hyperlocalPlain.length, initVector); System.arraycopy(hyperlocalPlain, 0, plainData, PAYLOAD_BASE, hyperlocalPlain.length); return encrypt(plainData); } /** * Decrypts the serialized {@code HyperlocalSet}. * * @param hyperlocalCipher encrypted {@code HyperlocalSet} * @return the {@code HyperLocalSet} */ public byte[] decryptHyperlocal(byte[] hyperlocalCipher) throws SignatureException { byte[] plainData = decrypt(hyperlocalCipher); return Arrays.copyOfRange(plainData, PAYLOAD_BASE, plainData.length - SIGNATURE_SIZE); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy