Replace usage of Java's, JGit's and Apache Commons' Base64 classes with BaseEncoding from Guava. No Gerrit code directly depends on Apache Commons Codec any more, so we can remove all the explicit dependencies from the build rules. However, it is still a transient dependency of the httpclient library, and is used by some plugins, so we can't completely remove it from the build. Change-Id: I9a934bda3e4519f359759b20efb298d544f1391b
206 lines
6.6 KiB
Java
206 lines
6.6 KiB
Java
// 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;
|
|
|
|
/**
|
|
* 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 encodeBase64(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(decodeBase64(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.
|
|
*
|
|
* @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.
|
|
*/
|
|
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 BaseEncoding.base64().decode(s);
|
|
}
|
|
|
|
private static String encodeBase64(final byte[] buf) {
|
|
return BaseEncoding.base64().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;
|
|
}
|
|
}
|