mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-27 14:22:47 +00:00
SEC-811: Provide a mechanism to allocate and rebuild cryptographically strong, randomised tokens.
This commit is contained in:
parent
a599ef5398
commit
7a2e1e13d3
@ -0,0 +1,59 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default implementation of {@link Token}.
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
public class DefaultToken implements Token {
|
||||||
|
private String key;
|
||||||
|
private long keyCreationTime;
|
||||||
|
private String extendedInformation;
|
||||||
|
|
||||||
|
public DefaultToken(String key, long keyCreationTime, String extendedInformation) {
|
||||||
|
Assert.hasText(key, "Key required");
|
||||||
|
Assert.notNull(extendedInformation, "Extended information cannot be null");
|
||||||
|
this.key = key;
|
||||||
|
this.keyCreationTime = keyCreationTime;
|
||||||
|
this.extendedInformation = extendedInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getKeyCreationTime() {
|
||||||
|
return keyCreationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExtendedInformation() {
|
||||||
|
return extendedInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj != null && obj instanceof DefaultToken) {
|
||||||
|
DefaultToken rhs = (DefaultToken) obj;
|
||||||
|
return this.key.equals(rhs.key) && this.keyCreationTime == rhs.keyCreationTime && this.extendedInformation.equals(rhs.extendedInformation);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hashCode() {
|
||||||
|
int code = 979;
|
||||||
|
code = code * key.hashCode();
|
||||||
|
code = code * new Long(keyCreationTime).hashCode();
|
||||||
|
code = code * extendedInformation.hashCode();
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return "DefaultToken[key=" + new String(key) + "; creation=" + new Date(keyCreationTime) + "; extended=" + extendedInformation + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.security.util.Sha512DigestUtils;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic implementation of {@link TokenService} that is compatible with clusters and across machine restarts,
|
||||||
|
* without requiring database persistence.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Keys are produced in the format:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Base64(creationTime + ":" + hex(pseudoRandomNumber) + ":" + extendedInformation + ":" +
|
||||||
|
* Sha512Hex(creationTime + ":" + hex(pseudoRandomNumber) + ":" + extendedInformation + ":" + serverSecret) )
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* In the above, <code>creationTime</code>, <code>tokenKey</code> and <code>extendedInformation</code>
|
||||||
|
* are equal to that stored in {@link Token}. The <code>Sha512Hex</code> includes the same payload,
|
||||||
|
* plus a <code>serverSecret</code>.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The <code>serverSecret</code> varies every millisecond. It relies on two static server-side secrets. The first
|
||||||
|
* is a password, and the second is a server integer. Both of these must remain the same for any issued keys
|
||||||
|
* to subsequently be recognised. The applicable <code>serverSecret</code> in any millisecond is computed by
|
||||||
|
* <code>password</code> + ":" + (<code>creationTime</code> % <code>serverInteger</code>). This approach
|
||||||
|
* further obfuscates the actual server secret and renders attempts to compute the server secret more
|
||||||
|
* limited in usefulness (as any false tokens would be forced to have a <code>creationTime</code> equal
|
||||||
|
* to the computed hash). Recall that framework features depending on token services should reject tokens
|
||||||
|
* that are relatively old in any event.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A further consideration of this class is the requirement for cryptographically strong pseudo-random numbers.
|
||||||
|
* To this end, the use of {@link SecureRandomFactoryBean} is recommended to inject the property.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation uses UTF-8 encoding internally for string manipulation.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class KeyBasedPersistenceTokenService implements TokenService, InitializingBean {
|
||||||
|
private int pseudoRandomNumberBits = 256;
|
||||||
|
private String serverSecret;
|
||||||
|
private Integer serverInteger;
|
||||||
|
private SecureRandom secureRandom;
|
||||||
|
|
||||||
|
public Token allocateToken(String extendedInformation) {
|
||||||
|
Assert.notNull(extendedInformation, "Must provided non-null extendedInformation (but it can be empty)");
|
||||||
|
long creationTime = new Date().getTime();
|
||||||
|
String serverSecret = computeServerSecretApplicableAt(creationTime);
|
||||||
|
String pseudoRandomNumber = generatePseudoRandomNumber();
|
||||||
|
String content = new Long(creationTime).toString() + ":" + pseudoRandomNumber + ":" + extendedInformation;
|
||||||
|
|
||||||
|
// Compute key
|
||||||
|
String sha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
|
||||||
|
String keyPayload = content + ":" + sha512Hex;
|
||||||
|
String key = convertToString(Base64.encodeBase64(convertToBytes(keyPayload)));
|
||||||
|
|
||||||
|
return new DefaultToken(key, creationTime, extendedInformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Token verifyToken(String key) {
|
||||||
|
if (key == null || "".equals(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] tokens = StringUtils.delimitedListToStringArray(convertToString(Base64.decodeBase64(convertToBytes(key))), ":");
|
||||||
|
Assert.isTrue(tokens.length >= 4, "Expected 4 or more tokens but found " + tokens.length);
|
||||||
|
|
||||||
|
long creationTime;
|
||||||
|
try {
|
||||||
|
creationTime = Long.decode(tokens[0]).longValue();
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
throw new IllegalArgumentException("Expected number but found " + tokens[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String serverSecret = computeServerSecretApplicableAt(creationTime);
|
||||||
|
String pseudoRandomNumber = tokens[1];
|
||||||
|
|
||||||
|
// Permit extendedInfo to itself contain ":" characters
|
||||||
|
StringBuffer extendedInfo = new StringBuffer();
|
||||||
|
for (int i = 2; i < tokens.length-1; i++) {
|
||||||
|
if (i > 2) {
|
||||||
|
extendedInfo.append(":");
|
||||||
|
}
|
||||||
|
extendedInfo.append(tokens[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String sha1Hex = tokens[tokens.length-1];
|
||||||
|
|
||||||
|
// Verification
|
||||||
|
String content = new Long(creationTime).toString() + ":" + pseudoRandomNumber + ":" + extendedInfo.toString();
|
||||||
|
String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
|
||||||
|
Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure");
|
||||||
|
|
||||||
|
return new DefaultToken(key, creationTime, extendedInfo.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] convertToBytes(String input) {
|
||||||
|
try {
|
||||||
|
return input.getBytes("UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertToString(byte[] bytes) {
|
||||||
|
try {
|
||||||
|
return new String(bytes, "UTF-8");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a pseduo random number (hex encoded)
|
||||||
|
*/
|
||||||
|
private String generatePseudoRandomNumber() {
|
||||||
|
byte[] randomizedBits = new byte[pseudoRandomNumberBits];
|
||||||
|
secureRandom.nextBytes(randomizedBits);
|
||||||
|
return new String(Hex.encodeHex(randomizedBits));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeServerSecretApplicableAt(long time) {
|
||||||
|
return serverSecret + ":" + new Long(time % serverInteger.intValue()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param serverSecret the new secret, which can contain a ":" if desired (never being sent to the client)
|
||||||
|
*/
|
||||||
|
public void setServerSecret(String serverSecret) {
|
||||||
|
this.serverSecret = serverSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecureRandom(SecureRandom secureRandom) {
|
||||||
|
this.secureRandom = secureRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param pseudoRandomNumberBits changes the number of bits issued (must be >= 0; defaults to 256)
|
||||||
|
*/
|
||||||
|
public void setPseudoRandomNumberBits(int pseudoRandomNumberBits) {
|
||||||
|
Assert.isTrue(pseudoRandomNumberBits >= 0, "Must have a positive pseudo random number bit size");
|
||||||
|
this.pseudoRandomNumberBits = pseudoRandomNumberBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setServerInteger(Integer serverInteger) {
|
||||||
|
this.serverInteger = serverInteger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
Assert.hasText(serverSecret, "Server secret required");
|
||||||
|
Assert.notNull(serverInteger, "Server integer required");
|
||||||
|
Assert.notNull(secureRandom, "SecureRandom instance required");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SecureRandom} instance.
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
* @since 2.0.1
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class SecureRandomFactoryBean implements FactoryBean {
|
||||||
|
|
||||||
|
private String algorithm = "SHA1PRNG";
|
||||||
|
private Resource seed;
|
||||||
|
|
||||||
|
public Object getObject() throws Exception {
|
||||||
|
SecureRandom rnd = SecureRandom.getInstance(algorithm);
|
||||||
|
|
||||||
|
if (seed != null) {
|
||||||
|
// Seed specified, so use it
|
||||||
|
byte[] seedBytes = FileCopyUtils.copyToByteArray(seed.getInputStream());
|
||||||
|
rnd.setSeed(seedBytes);
|
||||||
|
} else {
|
||||||
|
// Request the next bytes, thus eagerly incurring the expense of default seeding
|
||||||
|
rnd.nextBytes(new byte[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class getObjectType() {
|
||||||
|
return SecureRandom.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSingleton() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the Pseudo Random Number Generator (PRNG) algorithm to be nominated. Defaults to
|
||||||
|
* SHA1PRNG.
|
||||||
|
*
|
||||||
|
* @param algorithm to use (mandatory)
|
||||||
|
*/
|
||||||
|
public void setAlgorithm(String algorithm) {
|
||||||
|
Assert.hasText(algorithm, "Algorithm required");
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the user to specify a resource which will act as a seed for the {@link SecureRandom}
|
||||||
|
* instance. Specifically, the resource will be read into an {@link InputStream} and those
|
||||||
|
* bytes presented to the {@link SecureRandom#setSeed(byte[])} method. Note that this will
|
||||||
|
* simply supplement, rather than replace, the existing seed. As such, it is always safe to
|
||||||
|
* set a seed using this method (it never reduces randomness).
|
||||||
|
*
|
||||||
|
* @param seed to use, or <code>null</code> if no additional seeding is needed
|
||||||
|
*/
|
||||||
|
public void setSeed(Resource seed) {
|
||||||
|
this.seed = seed;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A token issued by {@link TokenService}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It is important that the keys assigned to tokens are sufficiently randomised and secured that
|
||||||
|
* they can serve as identifying a unique user session. Implementations of {@link TokenService}
|
||||||
|
* are free to use encryption or encoding strategies of their choice. It is strongly recommended that
|
||||||
|
* keys are of sufficient length to balance safety against persistence cost. In relation to persistence
|
||||||
|
* cost, it is strongly recommended that returned keys are small enough for encoding in a cookie.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
* @since 2.0.1
|
||||||
|
*/
|
||||||
|
public interface Token {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the randomised, secure key assigned to this token. Presentation of this token to
|
||||||
|
* {@link TokenService} will always return a <code>Token</code> that is equal to the original
|
||||||
|
* <code>Token</code> issued for that key.
|
||||||
|
*
|
||||||
|
* @return a key with appropriate randomness and security.
|
||||||
|
*/
|
||||||
|
String getKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the token key was initially created is available from this method. Note that a given
|
||||||
|
* token must never have this creation time changed. If necessary, a new token can be
|
||||||
|
* requested from the {@link TokenService} to replace the original token.
|
||||||
|
*
|
||||||
|
* @return the time this token key was created, in the same format as specified by {@link Date#getTime()).
|
||||||
|
*/
|
||||||
|
long getKeyCreationTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the extended information associated within the token, which was presented when the token
|
||||||
|
* was first created.
|
||||||
|
*
|
||||||
|
* @return the user-specified extended information, if any
|
||||||
|
*/
|
||||||
|
String getExtendedInformation();
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a mechanism to allocate and rebuild secure, randomised tokens.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Implementations are solely concern with issuing a new {@link Token} on demand. The
|
||||||
|
* issued <code>Token</code> may contain user-specified extended information. The token also
|
||||||
|
* contains a cryptographically strong, byte array-based key. This permits the token to be
|
||||||
|
* used to identify a user session, if desired. The key can subsequently be re-presented
|
||||||
|
* to the <code>TokenService</code> for verification and reconstruction of a <code>Token</code>
|
||||||
|
* equal to the original <code>Token</code>.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Given the tightly-focused behaviour provided by this interface, it can serve as a building block
|
||||||
|
* for more sophisticated token-based solutions. For example, authentication systems that depend on
|
||||||
|
* stateless session keys. These could, for instance, place the username inside the user-specified
|
||||||
|
* extended information associated with the key). It is important to recognise that we do not intend
|
||||||
|
* for this interface to be expanded to provide such capabilities directly.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
* @since 2.0.1
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public interface TokenService {
|
||||||
|
/**
|
||||||
|
* Forces the allocation of a new {@link Token}.
|
||||||
|
*
|
||||||
|
* @param the extended information desired in the token (cannot be <code>null</code>, but can be empty)
|
||||||
|
* @return a new token that has not been issued previously, and is guaranteed to be recognised
|
||||||
|
* by this implementation's {@link #verifyToken(String)} at any future time.
|
||||||
|
*/
|
||||||
|
Token allocateToken(String extendedInformation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permits verification the <{@link Token#getKey()} was issued by this <code>TokenService</code> and
|
||||||
|
* reconstructs the corresponding <code>Token</code>.
|
||||||
|
*
|
||||||
|
* @param key as obtained from {@link Token#getKey()} and created by this implementation
|
||||||
|
* @return the token, or <code>null</code> if the token was not issued by this <code>TokenService</code>
|
||||||
|
*/
|
||||||
|
Token verifyToken(String key);
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests {@link DefaultToken}.
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class DefaultTokenTests {
|
||||||
|
@Test
|
||||||
|
public void testEquality() {
|
||||||
|
String key = "key";
|
||||||
|
long created = new Date().getTime();
|
||||||
|
String extendedInformation = "extended";
|
||||||
|
|
||||||
|
DefaultToken t1 = new DefaultToken(key, created, extendedInformation);
|
||||||
|
DefaultToken t2 = new DefaultToken(key, created, extendedInformation);
|
||||||
|
Assert.assertEquals(t1, t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected=IllegalArgumentException.class)
|
||||||
|
public void testRejectsNullExtendedInformation() {
|
||||||
|
String key = "key";
|
||||||
|
long created = new Date().getTime();
|
||||||
|
new DefaultToken(key, created, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEqualityWithDifferentExtendedInformation3() {
|
||||||
|
String key = "key";
|
||||||
|
long created = new Date().getTime();
|
||||||
|
|
||||||
|
DefaultToken t1 = new DefaultToken(key, created, "length1");
|
||||||
|
DefaultToken t2 = new DefaultToken(key, created, "longerLength2");
|
||||||
|
Assert.assertFalse(t1.equals(t2));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
|
||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests {@link KeyBasedPersistenceTokenService}.
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class KeyBasedPersistenceTokenServiceTests {
|
||||||
|
|
||||||
|
private KeyBasedPersistenceTokenService getService() {
|
||||||
|
SecureRandomFactoryBean fb = new SecureRandomFactoryBean();
|
||||||
|
KeyBasedPersistenceTokenService service = new KeyBasedPersistenceTokenService();
|
||||||
|
service.setServerSecret("MY:SECRET$$$#");
|
||||||
|
service.setServerInteger(new Integer(454545));
|
||||||
|
try {
|
||||||
|
SecureRandom rnd = (SecureRandom) fb.getObject();
|
||||||
|
service.setSecureRandom(rnd);
|
||||||
|
service.afterPropertiesSet();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOperationWithSimpleExtendedInformation() {
|
||||||
|
KeyBasedPersistenceTokenService service = getService();
|
||||||
|
Token token = service.allocateToken("Hello world");
|
||||||
|
Token result = service.verifyToken(token.getKey());
|
||||||
|
Assert.assertEquals(token, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOperationWithComplexExtendedInformation() {
|
||||||
|
KeyBasedPersistenceTokenService service = getService();
|
||||||
|
Token token = service.allocateToken("Hello:world:::");
|
||||||
|
Token result = service.verifyToken(token.getKey());
|
||||||
|
Assert.assertEquals(token, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOperationWithEmptyRandomNumber() {
|
||||||
|
KeyBasedPersistenceTokenService service = getService();
|
||||||
|
service.setPseudoRandomNumberBits(0);
|
||||||
|
Token token = service.allocateToken("Hello:world:::");
|
||||||
|
Token result = service.verifyToken(token.getKey());
|
||||||
|
Assert.assertEquals(token, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOperationWithNoExtendedInformation() {
|
||||||
|
KeyBasedPersistenceTokenService service = getService();
|
||||||
|
Token token = service.allocateToken("");
|
||||||
|
Token result = service.verifyToken(token.getKey());
|
||||||
|
Assert.assertEquals(token, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected=IllegalArgumentException.class)
|
||||||
|
public void testOperationWithMissingKey() {
|
||||||
|
KeyBasedPersistenceTokenService service = getService();
|
||||||
|
Token token = new DefaultToken("", new Date().getTime(), "");
|
||||||
|
service.verifyToken(token.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected=IllegalArgumentException.class)
|
||||||
|
public void testOperationWithTamperedKey() {
|
||||||
|
KeyBasedPersistenceTokenService service = getService();
|
||||||
|
Token goodToken = service.allocateToken("");
|
||||||
|
String fake = goodToken.getKey().toUpperCase();
|
||||||
|
Token token = new DefaultToken(fake, new Date().getTime(), "");
|
||||||
|
service.verifyToken(token.getKey());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package org.springframework.security.token;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
import junit.framework.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests {@link SecureRandomFactoryBean}.
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class SecureRandomFactoryBeanTests {
|
||||||
|
@Test
|
||||||
|
public void testObjectType() {
|
||||||
|
SecureRandomFactoryBean factory = new SecureRandomFactoryBean();
|
||||||
|
Assert.assertEquals(SecureRandom.class, factory.getObjectType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIsSingleton() {
|
||||||
|
SecureRandomFactoryBean factory = new SecureRandomFactoryBean();
|
||||||
|
Assert.assertFalse(factory.isSingleton());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatesUsingDefaults() throws Exception {
|
||||||
|
SecureRandomFactoryBean factory = new SecureRandomFactoryBean();
|
||||||
|
Object result = factory.getObject();
|
||||||
|
Assert.assertTrue(result instanceof SecureRandom);
|
||||||
|
int rnd = ((SecureRandom)result).nextInt();
|
||||||
|
Assert.assertTrue(rnd != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatesUsingSeed() throws Exception {
|
||||||
|
SecureRandomFactoryBean factory = new SecureRandomFactoryBean();
|
||||||
|
Resource resource = new ClassPathResource("org/springframework/security/token/SecureRandomFactoryBeanTests.class");
|
||||||
|
Assert.assertNotNull(resource);
|
||||||
|
factory.setSeed(resource);
|
||||||
|
Object result = factory.getObject();
|
||||||
|
Assert.assertTrue(result instanceof SecureRandom);
|
||||||
|
int rnd = ((SecureRandom)result).nextInt();
|
||||||
|
Assert.assertTrue(rnd != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package org.springframework.security.util;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides SHA512 digest methods.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Based on Commons Codec, which does not presently provide SHA512 support.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Ben Alex
|
||||||
|
* @since 2.0.1
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public abstract class Sha512DigestUtils {
|
||||||
|
/**
|
||||||
|
* Returns a MessageDigest for the given <code>algorithm</code>.
|
||||||
|
*
|
||||||
|
* @param algorithm The MessageDigest algorithm name.
|
||||||
|
* @return An MD5 digest instance.
|
||||||
|
* @throws RuntimeException when a {@link java.security.NoSuchAlgorithmException} is caught,
|
||||||
|
*/
|
||||||
|
static MessageDigest getDigest(String algorithm) {
|
||||||
|
try {
|
||||||
|
return MessageDigest.getInstance(algorithm);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an SHA digest.
|
||||||
|
*
|
||||||
|
* @return An SHA digest instance.
|
||||||
|
* @throws RuntimeException when a {@link java.security.NoSuchAlgorithmException} is caught,
|
||||||
|
*/
|
||||||
|
private static MessageDigest getSha512Digest() {
|
||||||
|
return getDigest("SHA-512");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA digest and returns the value as a
|
||||||
|
* <code>byte[]</code>.
|
||||||
|
*
|
||||||
|
* @param data Data to digest
|
||||||
|
* @return SHA digest
|
||||||
|
*/
|
||||||
|
public static byte[] sha(byte[] data) {
|
||||||
|
return getSha512Digest().digest(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA digest and returns the value as a
|
||||||
|
* <code>byte[]</code>.
|
||||||
|
*
|
||||||
|
* @param data Data to digest
|
||||||
|
* @return SHA digest
|
||||||
|
*/
|
||||||
|
public static byte[] sha(String data) {
|
||||||
|
return sha(data.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA digest and returns the value as a hex string.
|
||||||
|
*
|
||||||
|
* @param data Data to digest
|
||||||
|
* @return SHA digest as a hex string
|
||||||
|
*/
|
||||||
|
public static String shaHex(byte[] data) {
|
||||||
|
return new String(Hex.encodeHex(sha(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the SHA digest and returns the value as a hex string.
|
||||||
|
*
|
||||||
|
* @param data Data to digest
|
||||||
|
* @return SHA digest as a hex string
|
||||||
|
*/
|
||||||
|
public static String shaHex(String data) {
|
||||||
|
return new String(Hex.encodeHex(sha(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user