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