| // Copyright 2008 Google Inc. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.server.mail; | |
| import com.google.common.io.BaseEncoding; | |
| import java.security.InvalidKeyException; | |
| import java.security.NoSuchAlgorithmException; | |
| import java.security.SecureRandom; | |
| import java.util.Arrays; | |
| import javax.crypto.Mac; | |
| import javax.crypto.ShortBufferException; | |
| import javax.crypto.spec.SecretKeySpec; | |
| import org.apache.commons.codec.binary.Base64; | |
| /** | |
| * Utility function to compute and verify XSRF tokens. | |
| * | |
| * <p>{@link SignedTokenEmailTokenVerifier} uses this class to verify tokens appearing in the custom | |
| * <code>xsrfKey | |
| * </code> JSON request property. The tokens protect against cross-site request forgery by depending | |
| * upon the browser's security model. The classic browser security model prohibits a script from | |
| * site A from reading any data received from site B. By sending unforgeable tokens from the server | |
| * and asking the client to return them to us, the client script must have had read access to the | |
| * token at some point and is therefore also from our server. | |
| */ | |
| public class SignedToken { | |
| private static final int INT_SZ = 4; | |
| private static final String MAC_ALG = "HmacSHA1"; | |
| /** | |
| * Generate a random key for use with the XSRF library. | |
| * | |
| * @return a new private key, base 64 encoded. | |
| */ | |
| public static String generateRandomKey() { | |
| final byte[] r = new byte[26]; | |
| new SecureRandom().nextBytes(r); | |
| return encodeBase64PrivateKey(r); | |
| } | |
| private final int maxAge; | |
| private final SecretKeySpec key; | |
| private final SecureRandom rng; | |
| private final int tokenLength; | |
| /** | |
| * Create a new utility, using the specific key. | |
| * | |
| * @param age the number of seconds a token may remain valid. | |
| * @param keyBase64 base 64 encoded representation of the key. | |
| * @throws XsrfException the JVM doesn't support the necessary algorithms. | |
| */ | |
| public SignedToken(final int age, final String keyBase64) throws XsrfException { | |
| maxAge = age > 5 ? age / 5 : age; | |
| key = new SecretKeySpec(decodeBase64PrivateKey(keyBase64), MAC_ALG); | |
| rng = new SecureRandom(); | |
| tokenLength = 2 * INT_SZ + newMac().getMacLength(); | |
| } | |
| /** | |
| * Generate a new signed token. | |
| * | |
| * @param text the text string to sign. Typically this should be some user-specific string, to | |
| * prevent replay attacks. The text must be safe to appear in whatever context the token | |
| * itself will appear, as the text is included on the end of the token. | |
| * @return the signed token. The text passed in <code>text</code> will appear after the first ',' | |
| * in the returned token string. | |
| * @throws XsrfException the JVM doesn't support the necessary algorithms. | |
| */ | |
| String newToken(final String text) throws XsrfException { | |
| final int q = rng.nextInt(); | |
| final byte[] buf = new byte[tokenLength]; | |
| encodeInt(buf, 0, q); | |
| encodeInt(buf, INT_SZ, now() ^ q); | |
| computeToken(buf, text); | |
| return encodeBase64(buf) + '$' + text; | |
| } | |
| /** | |
| * Validate a returned token. If the token is valid then return a {@link ValidToken}, else will | |
| * throw {@link XsrfException} when it's an unexpected token overflow or {@link | |
| * CheckTokenException} when it's an illegal token string format. | |
| * | |
| * @param tokenString a token string previously created by this class. | |
| * @param text text that must have been used during {@link #newToken(String)} in order for the | |
| * token to be valid. If null the text will be taken from the token string itself. | |
| * @return the token which is valid. | |
| * @throws XsrfException the JVM doesn't support the necessary algorithms to generate a token. | |
| * XSRF services are simply not available. | |
| * @throws CheckTokenException throws when token is null, the empty string, has expired, does not | |
| * match the text supplied, or is a forged token. | |
| */ | |
| public ValidToken checkToken(final String tokenString, final String text) | |
| throws XsrfException, CheckTokenException { | |
| if (tokenString == null || tokenString.length() == 0) { | |
| throw new CheckTokenException("Empty token"); | |
| } | |
| final int s = tokenString.indexOf('$'); | |
| if (s <= 0) { | |
| throw new CheckTokenException("Token does not contain character '$'"); | |
| } | |
| final String recvText = tokenString.substring(s + 1); | |
| final byte[] in; | |
| try { | |
| in = decodeBase64(tokenString.substring(0, s)); | |
| } catch (RuntimeException e) { | |
| throw new CheckTokenException("Base64 decoding failed", e); | |
| } | |
| if (in.length != tokenLength) { | |
| throw new CheckTokenException("Token length mismatch"); | |
| } | |
| final int q = decodeInt(in, 0); | |
| final int c = decodeInt(in, INT_SZ) ^ q; | |
| final int n = now(); | |
| if (maxAge > 0 && Math.abs(c - n) > maxAge) { | |
| throw new CheckTokenException("Token is expired"); | |
| } | |
| final byte[] gen = new byte[tokenLength]; | |
| System.arraycopy(in, 0, gen, 0, 2 * INT_SZ); | |
| computeToken(gen, text != null ? text : recvText); | |
| if (!Arrays.equals(gen, in)) { | |
| throw new CheckTokenException("Token text mismatch"); | |
| } | |
| return new ValidToken(maxAge > 0 && c + (maxAge >> 1) <= n, recvText); | |
| } | |
| private void computeToken(final byte[] buf, final String text) throws XsrfException { | |
| final Mac m = newMac(); | |
| m.update(buf, 0, 2 * INT_SZ); | |
| m.update(toBytes(text)); | |
| try { | |
| m.doFinal(buf, 2 * INT_SZ); | |
| } catch (ShortBufferException e) { | |
| throw new XsrfException("Unexpected token overflow", e); | |
| } | |
| } | |
| private Mac newMac() throws XsrfException { | |
| try { | |
| final Mac m = Mac.getInstance(MAC_ALG); | |
| m.init(key); | |
| return m; | |
| } catch (NoSuchAlgorithmException e) { | |
| throw new XsrfException(MAC_ALG + " not supported", e); | |
| } catch (InvalidKeyException e) { | |
| throw new XsrfException("Invalid private key", e); | |
| } | |
| } | |
| private static int now() { | |
| return (int) (System.currentTimeMillis() / 5000L); | |
| } | |
| private static byte[] decodeBase64PrivateKey(final String privateKeyBase64String) { | |
| return Base64.decodeBase64(toBytes(privateKeyBase64String)); | |
| } | |
| private static String encodeBase64PrivateKey(final byte[] buf) { | |
| return toString(Base64.encodeBase64(buf)); | |
| } | |
| private static byte[] decodeBase64(final String s) { | |
| return BaseEncoding.base64Url().decode(s); | |
| } | |
| private static String encodeBase64(final byte[] buf) { | |
| return BaseEncoding.base64Url().encode(buf); | |
| } | |
| private static void encodeInt(final byte[] buf, final int o, final int v) { | |
| int _v = v; | |
| buf[o + 3] = (byte) _v; | |
| _v >>>= 8; | |
| buf[o + 2] = (byte) _v; | |
| _v >>>= 8; | |
| buf[o + 1] = (byte) _v; | |
| _v >>>= 8; | |
| buf[o] = (byte) _v; | |
| } | |
| private static int decodeInt(final byte[] buf, final int o) { | |
| int r = buf[o] << 8; | |
| r |= buf[o + 1] & 0xff; | |
| r <<= 8; | |
| r |= buf[o + 2] & 0xff; | |
| return (r << 8) | (buf[o + 3] & 0xff); | |
| } | |
| private static byte[] toBytes(final String s) { | |
| final byte[] r = new byte[s.length()]; | |
| for (int k = r.length - 1; k >= 0; k--) { | |
| r[k] = (byte) s.charAt(k); | |
| } | |
| return r; | |
| } | |
| private static String toString(final byte[] b) { | |
| final StringBuilder r = new StringBuilder(b.length); | |
| for (int i = 0; i < b.length; i++) { | |
| r.append((char) b[i]); | |
| } | |
| return r.toString(); | |
| } | |
| } |