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