Java tutorial
// 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.gwtjsonrpc.server; import com.google.gwtjsonrpc.client.CookieAccess; import org.apache.commons.codec.binary.Base64; 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; /** * Utility function to compute and verify XSRF tokens. * <p> * {@link JsonServlet} 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 encodeBase64(r); } private final int maxAge; private final SecretKeySpec key; private final SecureRandom rng; private final int tokenLength; /** * Create a new utility, using a randomly generated key. * * @param age the number of seconds a token may remain valid. * @throws XsrfException the JVM doesn't support the necessary algorithms. */ public SignedToken(final int age) throws XsrfException { this(age, generateRandomKey()); } /** * 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(decodeBase64(keyBase64), MAC_ALG); rng = new SecureRandom(); tokenLength = 2 * INT_SZ + newMac().getMacLength(); } /** @return maximum age of a signed token, in seconds. */ public int getMaxAge() { return maxAge > 0 ? maxAge * 5 : maxAge; } /** * Get the text of a signed token which is stored in a cookie. * * @param cookieName the name of the cookie to get the text from. * @return the signed text; null if the cookie is not set or the cookie's * token was forged. */ public String getCookieText(final String cookieName) { final String val = CookieAccess.get(cookieName); boolean ok; try { ok = checkToken(val, null) != null; } catch (XsrfException e) { ok = false; } return ok ? CookieAccess.getTokenText(cookieName) : null; } /** * 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. */ public 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. * * @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 true if the token is valid; false if the token is null, the empty * string, has expired, does not match the text supplied, or is a * forged token. * @throws XsrfException the JVM doesn't support the necessary algorithms to * generate a token. XSRF services are simply not available. */ public ValidToken checkToken(final String tokenString, final String text) throws XsrfException { if (tokenString == null || tokenString.length() == 0) { return null; } final int s = tokenString.indexOf('$'); if (s <= 0) { return null; } final String recvText = tokenString.substring(s + 1); final byte[] in; try { in = decodeBase64(tokenString.substring(0, s)); } catch (RuntimeException e) { return null; } if (in.length != tokenLength) { return null; } 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) { return null; } 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)) { return null; } 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[] decodeBase64(final String s) { return Base64.decodeBase64(toBytes(s)); } private static String encodeBase64(final byte[] buf) { return toString(Base64.encodeBase64(buf)); } private static void encodeInt(final byte[] buf, final int o, int 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(); } }