/* * Copyright (c) 2013 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.cloud.backend.config; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import org.apache.geronimo.mail.util.Base64Encoder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; /** * Utility class for preventing XSRF attacks. The class is NOT general purposes, but rather tailored * to this sample. For example it assumes that it is called from code that is hosted on App Engine * and there is a user signed in. */ class XSRFTokenUtility { private static final long DEFAULT_MAX_TIME_DIFF = 1000 * 60 * 10; private static final long MAX_TIME_DIFF_BACK = 1000 * 60; private static final String DELIM = ":"; /** * Gets a token that can be used to prevent XSRF attacks. * * @param action Identifies the action that the token can be used for (e.g., reading * configuration). * @param secretKey SecretKey unique to this deployment. * @return token * @throws NoSuchAlgorithmException if SHA1 algorithm is not available. * @throws IOException if encoding the token failed. */ static String getToken(String secretKey, String action) throws NoSuchAlgorithmException, IOException { if (action == null || action.isEmpty()) { throw new IllegalArgumentException("'action' argument cannot be empty"); } if (secretKey == null || secretKey.length() < 20) { throw new IllegalArgumentException("'secretKey' should be at least 20 characters"); } String time = String.valueOf(System.currentTimeMillis()); String clearString = buildTokenString(secretKey, action, time); return encodeTokenString(clearString) + DELIM + time; } /** * Verifies a token to prevent XSRF attacks. * * @param action Identifies the action that the token is needed for (e.g., reading configuration) * @param secretKey SecretKey unique to this deployment. * @param token Token passed by the caller. * @return true if token is valid; false otherwise. * @throws NoSuchAlgorithmException if SHA1 algorithm is not available. * @throws IOException if encoding the token failed. */ static boolean verifyToken(String secretKey, String action, String token) throws NoSuchAlgorithmException, IOException { if (action == null || action.isEmpty()) { throw new IllegalArgumentException("'action' argument cannot be empty"); } if (secretKey == null || secretKey.length() < 20) { throw new IllegalArgumentException("'secretKey' should be at least 20 characters"); } if (token == null || token.isEmpty()) { return false; } String[] tokenParts = token.split(DELIM); if (tokenParts.length != 2) { return false; } long currentTime = System.currentTimeMillis(); long postTime; try { postTime = Long.valueOf(tokenParts[1]); } catch (NumberFormatException e) { return false; } if (currentTime < (postTime - MAX_TIME_DIFF_BACK) || currentTime > (postTime + DEFAULT_MAX_TIME_DIFF)) { return false; } String expectedTokenString = buildTokenString(secretKey, action, tokenParts[1]); if (!verifySignature(tokenParts[0], expectedTokenString)) { // Request is suspicious. The signature didn't match even though the time stamp was OK. try { Thread.sleep(new Random().nextInt(2000)); } catch (InterruptedException e) { // ignore } return false; } return true; } private static String buildTokenString(String secretKey, String action, String time) { UserService userService = UserServiceFactory.getUserService(); User user = userService.getCurrentUser(); return buildTokenString(secretKey, user.getEmail(), action, time); } private static String buildTokenString(String secretKey, String user, String action, String time) { return secretKey + DELIM + user.replaceAll(DELIM, "_") + DELIM + action.replaceAll(DELIM, "_") + DELIM + time.replaceAll(DELIM, "_"); } private static String encodeTokenString(String tokenString) throws NoSuchAlgorithmException, IOException { MessageDigest tokenSigner = MessageDigest.getInstance("SHA-1"); byte[] token = tokenSigner.digest(tokenString.getBytes("UTF-8")); Base64Encoder encoder = new Base64Encoder(); ByteArrayOutputStream out = new ByteArrayOutputStream(); encoder.encode(token, 0, token.length, out); return out.toString(); } private static boolean verifySignature(String receivedToken, String expectedTokenString) throws NoSuchAlgorithmException, IOException { String expectedToken = encodeTokenString(expectedTokenString); return expectedToken.equals(receivedToken); } }