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}