001package com.box.sdk;
002
003import com.box.sdk.internal.pool.MacPool;
004import java.nio.charset.Charset;
005import java.security.InvalidKeyException;
006import java.security.MessageDigest;
007import java.util.Collections;
008import java.util.EnumSet;
009import java.util.Map;
010import java.util.Set;
011import java.util.concurrent.ConcurrentHashMap;
012import javax.crypto.Mac;
013import javax.crypto.spec.SecretKeySpec;
014
015/**
016 * Signature verifier for Webhook Payload.
017 *
018 * @since 2.2.1
019 */
020public class BoxWebHookSignatureVerifier {
021
022  /** Reference to UTF_8 {@link Charset}. */
023  private static final Charset UTF_8 = Charset.forName("UTF-8");
024
025  /** Versions supported by this implementation. */
026  private static final Set<String> SUPPORTED_VERSIONS = Collections.singleton("1");
027
028  /** Algorithms supported by this implementation. */
029  private static final Set<BoxSignatureAlgorithm> SUPPORTED_ALGORITHMS =
030      Collections.unmodifiableSet(EnumSet.of(BoxSignatureAlgorithm.HMAC_SHA256));
031
032  /** {@link Mac}-s pool. */
033  private static final MacPool MAC_POOL = new MacPool();
034
035  /** Primary key setup within the Box. */
036  private final String primarySignatureKey;
037
038  /** Secondary key setup within the Box. */
039  private final String secondarySignatureKey;
040
041  /**
042   * Creates a new instance of verifier specified with given primary and secondary keys. Primary key
043   * and secondary key are needed for rotating purposes, at least at one has to be valid.
044   *
045   * @param primarySignatureKey primary signature key for web-hooks (can not be null)
046   * @param secondarySignatureKey secondary signature key for web-hooks (can be null)
047   * @throws IllegalArgumentException primary key can not be null
048   */
049  public BoxWebHookSignatureVerifier(String primarySignatureKey, String secondarySignatureKey) {
050    if (primarySignatureKey == null && secondarySignatureKey == null) {
051      throw new IllegalArgumentException(
052          "At least primary or secondary signature key must be provided!");
053    }
054
055    this.primarySignatureKey = primarySignatureKey;
056    this.secondarySignatureKey = secondarySignatureKey;
057  }
058
059  /**
060   * Verifies given web-hook information.
061   *
062   * @param signatureVersion signature version received from web-hook
063   * @param signatureAlgorithm signature algorithm received from web-hook
064   * @param primarySignature primary signature received from web-hook
065   * @param secondarySignature secondary signature received from web-hook
066   * @param webHookPayload payload of web-hook
067   * @param deliveryTimestamp devilery timestamp received from web-hook
068   * @return true, if given payload is successfully verified against primary and secondary
069   *     signatures, false otherwise
070   */
071  public boolean verify(
072      String signatureVersion,
073      String signatureAlgorithm,
074      String primarySignature,
075      String secondarySignature,
076      String webHookPayload,
077      String deliveryTimestamp) {
078
079    // enforce versions supported by this implementation
080    if (!SUPPORTED_VERSIONS.contains(signatureVersion)) {
081      return false;
082    }
083
084    // enforce algorithms supported by this implementation
085    BoxSignatureAlgorithm algorithm = BoxSignatureAlgorithm.byName(signatureAlgorithm);
086    if (!SUPPORTED_ALGORITHMS.contains(algorithm)) {
087      return false;
088    }
089
090    // check primary key signature if primary key exists
091    if (this.primarySignatureKey != null
092        && this.verify(
093            this.primarySignatureKey,
094            algorithm,
095            primarySignature,
096            webHookPayload,
097            deliveryTimestamp)) {
098      return true;
099    }
100
101    // check secondary key signature if secondary key exists
102    if (this.secondarySignatureKey != null
103        && this.verify(
104            this.secondarySignatureKey,
105            algorithm,
106            secondarySignature,
107            webHookPayload,
108            deliveryTimestamp)) {
109      return true;
110    }
111
112    // default strategy is false, to minimize security issues
113    return false;
114  }
115
116  /**
117   * Verifies a provided signature.
118   *
119   * @param key for which signature key
120   * @param actualAlgorithm current signature algorithm
121   * @param actualSignature current signature
122   * @param webHookPayload for signing
123   * @param deliveryTimestamp for signing
124   * @return true if verification passed
125   */
126  private boolean verify(
127      String key,
128      BoxSignatureAlgorithm actualAlgorithm,
129      String actualSignature,
130      String webHookPayload,
131      String deliveryTimestamp) {
132    if (actualSignature == null) {
133      return false;
134    }
135
136    byte[] actual = Base64.decode(actualSignature);
137    byte[] expected = this.signRaw(actualAlgorithm, key, webHookPayload, deliveryTimestamp);
138
139    return MessageDigest.isEqual(expected, actual);
140  }
141
142  /**
143   * Calculates signature for a provided information.
144   *
145   * @param algorithm for which algorithm
146   * @param key used by signing
147   * @param webHookPayload for singing
148   * @param deliveryTimestamp for signing
149   * @return calculated signature
150   */
151  public String sign(
152      BoxSignatureAlgorithm algorithm,
153      String key,
154      String webHookPayload,
155      String deliveryTimestamp) {
156    return Base64.encode(this.signRaw(algorithm, key, webHookPayload, deliveryTimestamp));
157  }
158
159  /**
160   * Calculates signature for a provided information.
161   *
162   * @param algorithm for which algorithm
163   * @param key used by signing
164   * @param webHookPayload for singing
165   * @param deliveryTimestamp for signing
166   * @return calculated signature
167   */
168  private byte[] signRaw(
169      BoxSignatureAlgorithm algorithm,
170      String key,
171      String webHookPayload,
172      String deliveryTimestamp) {
173    Mac mac = MAC_POOL.acquire(algorithm.javaProviderName);
174    try {
175      mac.init(new SecretKeySpec(key.getBytes(UTF_8), algorithm.javaProviderName));
176      mac.update(UTF_8.encode(webHookPayload));
177      mac.update(UTF_8.encode(deliveryTimestamp));
178      return mac.doFinal();
179    } catch (InvalidKeyException e) {
180      throw new IllegalArgumentException("Invalid key: ", e);
181    } finally {
182      MAC_POOL.release(mac);
183    }
184  }
185
186  /** Box Signature Algorithms. */
187  public enum BoxSignatureAlgorithm {
188
189    /** HmacSHA256 algorithm. */
190    HMAC_SHA256("HmacSHA256", "HmacSHA256");
191
192    /** @see #byName(String) */
193    private static final Map<String, BoxSignatureAlgorithm> ALGORITHM_BY_NAME;
194
195    static {
196      Map<String, BoxSignatureAlgorithm> algorithmByName =
197          new ConcurrentHashMap<String, BoxSignatureAlgorithm>();
198      for (BoxSignatureAlgorithm algorithm : BoxSignatureAlgorithm.values()) {
199        algorithmByName.put(algorithm.name, algorithm);
200      }
201      ALGORITHM_BY_NAME = Collections.unmodifiableMap(algorithmByName);
202    }
203
204    /** Algorithm name by Box. */
205    private final String name;
206    /** Algorithm name according to the Java provider. */
207    private final String javaProviderName;
208
209    /**
210     * Constructor.
211     *
212     * @param name algorithm name by Box
213     * @param javaProviderName algorithm name according to the Java provider
214     */
215    BoxSignatureAlgorithm(String name, String javaProviderName) {
216      this.name = javaProviderName;
217      this.javaProviderName = javaProviderName;
218    }
219
220    /**
221     * Resolves {@link BoxSignatureAlgorithm} according to its name.
222     *
223     * @param name of algorithm
224     * @return resolved {@link BoxSignatureAlgorithm} or null if does not exist
225     */
226    private static BoxSignatureAlgorithm byName(String name) {
227      return ALGORITHM_BY_NAME.get(name);
228    }
229  }
230}