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}