Line data Source code
1 : // Copyright 2008 Google Inc. 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.mail; 16 : 17 : import com.google.common.io.BaseEncoding; 18 : import java.security.InvalidKeyException; 19 : import java.security.NoSuchAlgorithmException; 20 : import java.security.SecureRandom; 21 : import java.util.Arrays; 22 : import javax.crypto.Mac; 23 : import javax.crypto.ShortBufferException; 24 : import javax.crypto.spec.SecretKeySpec; 25 : import org.apache.commons.codec.binary.Base64; 26 : 27 : /** 28 : * Utility function to compute and verify XSRF tokens. 29 : * 30 : * <p>{@link SignedTokenEmailTokenVerifier} uses this class to verify tokens appearing in the custom 31 : * <code>xsrfKey 32 : * </code> JSON request property. The tokens protect against cross-site request forgery by depending 33 : * upon the browser's security model. The classic browser security model prohibits a script from 34 : * site A from reading any data received from site B. By sending unforgeable tokens from the server 35 : * and asking the client to return them to us, the client script must have had read access to the 36 : * token at some point and is therefore also from our server. 37 : */ 38 : public class SignedToken { 39 : private static final int INT_SZ = 4; 40 : private static final String MAC_ALG = "HmacSHA1"; 41 : 42 : /** 43 : * Generate a random key for use with the XSRF library. 44 : * 45 : * @return a new private key, base 64 encoded. 46 : */ 47 : public static String generateRandomKey() { 48 17 : final byte[] r = new byte[26]; 49 17 : new SecureRandom().nextBytes(r); 50 17 : return encodeBase64PrivateKey(r); 51 : } 52 : 53 : private final int maxAge; 54 : private final SecretKeySpec key; 55 : private final SecureRandom rng; 56 : private final int tokenLength; 57 : 58 : /** 59 : * Create a new utility, using the specific key. 60 : * 61 : * @param age the number of seconds a token may remain valid. 62 : * @param keyBase64 base 64 encoded representation of the key. 63 : * @throws XsrfException the JVM doesn't support the necessary algorithms. 64 : */ 65 20 : public SignedToken(final int age, final String keyBase64) throws XsrfException { 66 20 : maxAge = age > 5 ? age / 5 : age; 67 20 : key = new SecretKeySpec(decodeBase64PrivateKey(keyBase64), MAC_ALG); 68 20 : rng = new SecureRandom(); 69 20 : tokenLength = 2 * INT_SZ + newMac().getMacLength(); 70 20 : } 71 : 72 : /** 73 : * Generate a new signed token. 74 : * 75 : * @param text the text string to sign. Typically this should be some user-specific string, to 76 : * prevent replay attacks. The text must be safe to appear in whatever context the token 77 : * itself will appear, as the text is included on the end of the token. 78 : * @return the signed token. The text passed in <code>text</code> will appear after the first ',' 79 : * in the returned token string. 80 : * @throws XsrfException the JVM doesn't support the necessary algorithms. 81 : */ 82 : String newToken(final String text) throws XsrfException { 83 5 : final int q = rng.nextInt(); 84 5 : final byte[] buf = new byte[tokenLength]; 85 5 : encodeInt(buf, 0, q); 86 5 : encodeInt(buf, INT_SZ, now() ^ q); 87 5 : computeToken(buf, text); 88 5 : return encodeBase64(buf) + '$' + text; 89 : } 90 : 91 : /** 92 : * Validate a returned token. If the token is valid then return a {@link ValidToken}, else will 93 : * throw {@link XsrfException} when it's an unexpected token overflow or {@link 94 : * CheckTokenException} when it's an illegal token string format. 95 : * 96 : * @param tokenString a token string previously created by this class. 97 : * @param text text that must have been used during {@link #newToken(String)} in order for the 98 : * token to be valid. If null the text will be taken from the token string itself. 99 : * @return the token which is valid. 100 : * @throws XsrfException the JVM doesn't support the necessary algorithms to generate a token. 101 : * XSRF services are simply not available. 102 : * @throws CheckTokenException throws when token is null, the empty string, has expired, does not 103 : * match the text supplied, or is a forged token. 104 : */ 105 : public ValidToken checkToken(final String tokenString, final String text) 106 : throws XsrfException, CheckTokenException { 107 : 108 3 : if (tokenString == null || tokenString.length() == 0) { 109 2 : throw new CheckTokenException("Empty token"); 110 : } 111 : 112 3 : final int s = tokenString.indexOf('$'); 113 3 : if (s <= 0) { 114 3 : throw new CheckTokenException("Token does not contain character '$'"); 115 : } 116 : 117 3 : final String recvText = tokenString.substring(s + 1); 118 : final byte[] in; 119 : try { 120 3 : in = decodeBase64(tokenString.substring(0, s)); 121 2 : } catch (RuntimeException e) { 122 2 : throw new CheckTokenException("Base64 decoding failed", e); 123 3 : } 124 : 125 3 : if (in.length != tokenLength) { 126 1 : throw new CheckTokenException("Token length mismatch"); 127 : } 128 : 129 3 : final int q = decodeInt(in, 0); 130 3 : final int c = decodeInt(in, INT_SZ) ^ q; 131 3 : final int n = now(); 132 3 : if (maxAge > 0 && Math.abs(c - n) > maxAge) { 133 0 : throw new CheckTokenException("Token is expired"); 134 : } 135 : 136 3 : final byte[] gen = new byte[tokenLength]; 137 3 : System.arraycopy(in, 0, gen, 0, 2 * INT_SZ); 138 3 : computeToken(gen, text != null ? text : recvText); 139 3 : if (!Arrays.equals(gen, in)) { 140 2 : throw new CheckTokenException("Token text mismatch"); 141 : } 142 : 143 3 : return new ValidToken(maxAge > 0 && c + (maxAge >> 1) <= n, recvText); 144 : } 145 : 146 : private void computeToken(final byte[] buf, final String text) throws XsrfException { 147 5 : final Mac m = newMac(); 148 5 : m.update(buf, 0, 2 * INT_SZ); 149 5 : m.update(toBytes(text)); 150 : try { 151 5 : m.doFinal(buf, 2 * INT_SZ); 152 0 : } catch (ShortBufferException e) { 153 0 : throw new XsrfException("Unexpected token overflow", e); 154 5 : } 155 5 : } 156 : 157 : private Mac newMac() throws XsrfException { 158 : try { 159 20 : final Mac m = Mac.getInstance(MAC_ALG); 160 20 : m.init(key); 161 20 : return m; 162 0 : } catch (NoSuchAlgorithmException e) { 163 0 : throw new XsrfException(MAC_ALG + " not supported", e); 164 0 : } catch (InvalidKeyException e) { 165 0 : throw new XsrfException("Invalid private key", e); 166 : } 167 : } 168 : 169 : private static int now() { 170 5 : return (int) (System.currentTimeMillis() / 5000L); 171 : } 172 : 173 : private static byte[] decodeBase64PrivateKey(final String privateKeyBase64String) { 174 20 : return Base64.decodeBase64(toBytes(privateKeyBase64String)); 175 : } 176 : 177 : private static String encodeBase64PrivateKey(final byte[] buf) { 178 17 : return toString(Base64.encodeBase64(buf)); 179 : } 180 : 181 : private static byte[] decodeBase64(final String s) { 182 3 : return BaseEncoding.base64Url().decode(s); 183 : } 184 : 185 : private static String encodeBase64(final byte[] buf) { 186 5 : return BaseEncoding.base64Url().encode(buf); 187 : } 188 : 189 : private static void encodeInt(final byte[] buf, final int o, final int v) { 190 5 : int _v = v; 191 5 : buf[o + 3] = (byte) _v; 192 5 : _v >>>= 8; 193 : 194 5 : buf[o + 2] = (byte) _v; 195 5 : _v >>>= 8; 196 : 197 5 : buf[o + 1] = (byte) _v; 198 5 : _v >>>= 8; 199 : 200 5 : buf[o] = (byte) _v; 201 5 : } 202 : 203 : private static int decodeInt(final byte[] buf, final int o) { 204 3 : int r = buf[o] << 8; 205 : 206 3 : r |= buf[o + 1] & 0xff; 207 3 : r <<= 8; 208 : 209 3 : r |= buf[o + 2] & 0xff; 210 3 : return (r << 8) | (buf[o + 3] & 0xff); 211 : } 212 : 213 : private static byte[] toBytes(final String s) { 214 20 : final byte[] r = new byte[s.length()]; 215 20 : for (int k = r.length - 1; k >= 0; k--) { 216 20 : r[k] = (byte) s.charAt(k); 217 : } 218 20 : return r; 219 : } 220 : 221 : private static String toString(final byte[] b) { 222 17 : final StringBuilder r = new StringBuilder(b.length); 223 17 : for (int i = 0; i < b.length; i++) { 224 17 : r.append((char) b[i]); 225 : } 226 17 : return r.toString(); 227 : } 228 : }