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}