package com.fg.http.csrf; | |
import org.apache.commons.codec.binary.Base32; | |
import org.springframework.web.util.WebUtils; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpSession; | |
import java.security.SecureRandom; | |
import java.util.Random; | |
/** | |
* This class contains method for secure CSRF token generation and Breach protection. | |
* | |
* @author Jan Novotný (novotny@fg.cz), FG Forrest, a.s. | |
*/ | |
public class CsrfSupport { | |
private static final SecureRandom RANDOM_GENERATOR = new SecureRandom(); | |
private static final Random FAST_RANDOM_GENERATOR = new SecureRandom(); | |
private static final String CSRF_SESSION_TOKEN = "__CSRF_SESSION_TOKEN"; | |
private static final String REQUEST_CSRF_TOKEN = "__CSRF_REQUEST_TOKEN"; | |
private static final int CSRF_TOKEN_LENGTH = 20; | |
private static final int CSRF_ENCODED_TOKEN_LENGTH = 32; | |
private CsrfSupport() { } | |
/** | |
* Generates random 20B wide token in a secure way. | |
* @return | |
*/ | |
public static String generateRandomToken() { | |
return generateUniqueToken(20); | |
} | |
/** | |
* Generates secure random token of specified size. | |
* @param size | |
* @return | |
*/ | |
public static String generateUniqueToken(int size) { | |
final byte[] formId = new byte[size]; | |
RANDOM_GENERATOR.nextBytes(formId); | |
return new Base32().encodeAsString(formId); | |
} | |
/** | |
* Generates random bytes of specified count with less security requirements for the random generator. | |
* @param size | |
* @return | |
*/ | |
public static byte[] generateRandomBytes(int size) { | |
final byte[] randomBytes = new byte[size]; | |
FAST_RANDOM_GENERATOR.nextBytes(randomBytes); | |
return randomBytes; | |
} | |
/** | |
* Returns CSRF token from session if session already exists. | |
* | |
* @param request | |
* @return valid CSRF token if session exists, null if it does not | |
*/ | |
public static String getCsrfToken(HttpServletRequest request) { | |
if(request.getSession(false) == null) { | |
return null; | |
} else { | |
return getCsrfToken(request.getSession()); | |
} | |
} | |
/** | |
* Returns CSRF token from session or initializes new one if it hasn't been yet created. | |
* | |
* @param session | |
* @return valid CSRF token | |
*/ | |
public static String getCsrfToken(HttpSession session) { | |
final String csrfToken = (String)session.getAttribute(CSRF_SESSION_TOKEN); | |
if(csrfToken == null) { | |
return initCsrfToken(session); | |
} else { | |
return csrfToken; | |
} | |
} | |
/** | |
* Variant of {@link #encryptCsrfToken(String)} that involves caching of computed value to request attribute. | |
* @param request | |
* @return | |
*/ | |
public static String getEncryptedCsrfToken(HttpServletRequest request) { | |
String encryptedToken = (String)request.getAttribute(REQUEST_CSRF_TOKEN); | |
if (encryptedToken == null) { | |
encryptedToken = encryptCsrfToken(getCsrfToken(request)); | |
request.setAttribute(REQUEST_CSRF_TOKEN, encryptedToken); | |
} | |
return encryptedToken; | |
} | |
/** | |
* This method allows to generate pseudo random string that masks original CSRF token that doesn't change for | |
* entire session. Original CSRF token can be easily decrypted from the random string by XORing left part with | |
* the right one. | |
* | |
* This technique allows to mitigate Heist/Breach attacks that allow attacker to guess CSRF token very fast. | |
* By changing token contents on every request the attack is (for CSRF token only) mitigated. | |
* | |
* @return | |
*/ | |
public static String encryptCsrfToken(String csrfToken) { | |
final Base32 base32 = new Base32(); | |
final byte[] salt = generateRandomBytes(CSRF_TOKEN_LENGTH); | |
final byte[] csrf = base32.decode(csrfToken); | |
final int csrfLength = csrf.length; | |
final byte[] encrypted = new byte[csrfLength]; | |
for(int i = 0; i < csrfLength; i++) { | |
int c = csrf[i]; | |
int s = salt[i]; | |
encrypted[i] = (byte)(0xff & (c ^ s)); | |
} | |
return base32.encodeAsString(encrypted) + base32.encodeAsString(salt); | |
} | |
/** | |
* Decrypts CSRF token encrypted with {@link #encryptCsrfToken(String)} | |
* @param csrfToken | |
* @return | |
*/ | |
public static String decryptCsrfToken(String csrfToken) { | |
if (csrfToken.length() == CSRF_ENCODED_TOKEN_LENGTH) { | |
// non-encrypted variant passed in input | |
return csrfToken; | |
} else { | |
final Base32 base32 = new Base32(); | |
final byte[] csrf = base32.decode(csrfToken.substring(0, CSRF_ENCODED_TOKEN_LENGTH)); | |
final byte[] salt = base32.decode(csrfToken.substring(CSRF_ENCODED_TOKEN_LENGTH, csrfToken.length())); | |
final int csrfLength = csrf.length; | |
for(int i = 0; i < csrfLength; i++) { | |
byte c = csrf[i]; | |
byte s = salt[i]; | |
csrf[i] = (byte)(0xff & (c ^ s)); | |
} | |
return base32.encodeAsString(csrf); | |
} | |
} | |
/** | |
* Initializes CSRF token in session in case it hasn't already exist. | |
* | |
* @param session | |
* @return valid CSRF token | |
*/ | |
public static String initCsrfToken(HttpSession session) { | |
final Object mutex = WebUtils.getSessionMutex(session); | |
synchronized(mutex) { | |
final String token = generateUniqueToken(CSRF_TOKEN_LENGTH); | |
if(session.getAttribute(CSRF_SESSION_TOKEN) == null) { | |
session.setAttribute(CSRF_SESSION_TOKEN, token); | |
return token; | |
} else { | |
return (String)session.getAttribute(CSRF_SESSION_TOKEN); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment