LCOV - code coverage report
Current view: top level - server/mail - SignedToken.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 73 80 91.2 %
Date: 2022-11-19 15:00:39 Functions: 15 15 100.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750