package com.test.yuvagen.basic;
import java.security.SecureRandom;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.SecretKeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.xml.bind.DatatypeConverter;
public class
PasswordEncryption {
@SuppressWarnings("serial")
static public class
InvalidHashException extends
Exception {
public InvalidHashException(String message)
{
super(message);
}
public InvalidHashException(String message,
Throwable source) {
super(message, source);
}
}
@SuppressWarnings("serial")
static public class
CannotPerformOperationException extends
Exception {
public
CannotPerformOperationException(String message) {
super(message);
}
public
CannotPerformOperationException(String message, Throwable source) {
super(message, source);
}
}
public static final String
PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
//
These constants may be changed without breaking existing hashes.
public static final int SALT_BYTE_SIZE = 24;
public static final int HASH_BYTE_SIZE = 18;
public static final int PBKDF2_ITERATIONS = 64000;
//
These constants define the encoding and may not be changed.
public static final int HASH_SECTIONS = 5;
public static final int HASH_ALGORITHM_INDEX = 0;
public static final int ITERATION_INDEX = 1;
public static final int HASH_SIZE_INDEX = 2;
public static final int SALT_INDEX = 3;
public static final int PBKDF2_INDEX = 4;
public static String createHash(String password) throws CannotPerformOperationException {
return createHash(password.toCharArray());
}
public static String createHash(char[]
password) throws
CannotPerformOperationException {
//
Generate a random salt
SecureRandom
random = new SecureRandom();
byte[] salt = new byte[SALT_BYTE_SIZE];
random.nextBytes(salt);
//
Hash the password
byte[] hash = pbkdf2(password, salt,
PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
int hashSize = hash.length;
//
format: algorithm:iterations:hashSize:salt:hash
String
parts = "sha1:" + PBKDF2_ITERATIONS
+ ":" + hashSize + ":" + toBase64(salt) + ":"
+ toBase64(hash);
return parts;
}
public static boolean
verifyPassword(String password, String correctHash)
throws
CannotPerformOperationException, InvalidHashException {
return verifyPassword(password.toCharArray(),
correctHash);
}
public static boolean
verifyPassword(char[] password,
String correctHash)
throws
CannotPerformOperationException, InvalidHashException {
//
Decode the hash into its parameters
String[]
params = correctHash.split(":");
if (params.length != HASH_SECTIONS) {
throw new InvalidHashException("Fields are missing from the
password hash.");
}
//
Currently, Java only supports SHA1.
if (!params[HASH_ALGORITHM_INDEX].equals("sha1")) {
throw new CannotPerformOperationException("Unsupported hash
type.");
}
int iterations = 0;
try {
iterations
= Integer.parseInt(params[ITERATION_INDEX]);
}
catch (NumberFormatException ex)
{
throw new InvalidHashException("Could not parse the iteration count
as an integer.", ex);
}
if (iterations < 1) {
throw new InvalidHashException("Invalid number of iterations. Must
be >= 1.");
}
byte[] salt = null;
try {
salt
= fromBase64(params[SALT_INDEX]);
}
catch (IllegalArgumentException ex)
{
throw new InvalidHashException("Base64 decoding of salt
failed.", ex);
}
byte[] hash = null;
try {
hash
= fromBase64(params[PBKDF2_INDEX]);
}
catch (IllegalArgumentException ex)
{
throw new InvalidHashException("Base64 decoding of pbkdf2 output
failed.", ex);
}
int storedHashSize = 0;
try {
storedHashSize
= Integer.parseInt(params[HASH_SIZE_INDEX]);
}
catch (NumberFormatException ex)
{
throw new InvalidHashException("Could not parse the hash size as an
integer.", ex);
}
if (storedHashSize != hash.length) {
throw new InvalidHashException("Hash length doesn't match stored
hash length.");
}
//
Compute the hash of the provided password, using the same salt,
//
iteration count, and hash length
byte[] testHash = pbkdf2(password,
salt, iterations, hash.length);
//
Compare the hash in constant time. The password is correct if
//
both hashes match.
return slowEquals(hash, testHash);
}
private static boolean
slowEquals(byte[] a, byte[] b) {
int diff = a.length ^ b.length;
for (int i = 0; i < a.length && i < b.length; i++)
diff
|= a[i] ^ b[i];
return diff == 0;
}
private static byte[]
pbkdf2(char[] password, byte[] salt, int iterations, int
bytes)
throws CannotPerformOperationException
{
try {
PBEKeySpec
spec = new PBEKeySpec(password, salt,
iterations, bytes * 8);
SecretKeyFactory
skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
return skf.generateSecret(spec).getEncoded();
}
catch (NoSuchAlgorithmException ex)
{
throw new CannotPerformOperationException("Hash algorithm not
supported.", ex);
}
catch (InvalidKeySpecException ex)
{
throw new CannotPerformOperationException("Invalid key spec.",
ex);
}
}
private static byte[]
fromBase64(String hex) throws
IllegalArgumentException {
return DatatypeConverter.parseBase64Binary(hex);
}
private static String toBase64(byte[]
array) {
return DatatypeConverter.printBase64Binary(array);
}
}
package com.test.yuvagen.basic;
public class
Test {
public static void
main(String[] args) {
basicTests();
truncatedHashTest();
testHashFunctionChecking();
}
// Make
sure truncated hashes don't validate.
public static void
truncatedHashTest() {
String
userString = "password!";
String
goodHash = "";
String
badHash = "";
int badHashLength = 0;
try {
goodHash
= PasswordEncryption.createHash(userString);
}
catch (Exception e) {
System.out.println(e.getMessage());
System.exit(1);
}
badHashLength
= goodHash.length();
do {
badHashLength
-= 1;
badHash
= goodHash.substring(0, badHashLength);
boolean raised = false;
try {
PasswordEncryption.verifyPassword(userString,
badHash);
}
catch
(PasswordEncryption.InvalidHashException ex) {
raised
= true;
}
catch (Exception ex) {
System.out.println(ex.getMessage());
System.exit(1);
}
if (!raised) {
System.out.println("Truncated hash
test: FAIL " + "(At hash length of " + badHashLength + ")");
System.exit(1);
}
//
The loop goes on until it is two characters away from the last :
//
it
//
finds. This is because the PBKDF2 function requires a hash that's
//
at
//
least 2 characters long.
}
while (badHash.charAt(badHashLength
- 3) != ':');
System.out.println("Truncated hash
test: pass");
}
/**
* Tests the basic functionality of the
PasswordStorage class
*
* @param
args
*
ignored
*/
public static void
basicTests() {
try {
//
Test password validation
boolean failure = false;
for (int i = 0; i < 10; i++) {
String
password = "New" + i;
String
hash = PasswordEncryption.createHash(password);
String
secondHash = PasswordEncryption.createHash(password);
if (hash.equals(secondHash)) {
System.out.println("FAILURE: TWO
HASHES ARE EQUAL!");
failure
= true;
}
String
wrongPassword = "New" + (i + 1);
if (PasswordEncryption.verifyPassword(wrongPassword,
hash)) {
System.out.println("FAILURE: WRONG
PASSWORD ACCEPTED!");
failure
= true;
}
if (!PasswordEncryption.verifyPassword(password,
hash)) {
System.out.println("FAILURE: GOOD
PASSWORD NOT ACCEPTED!");
failure
= true;
}
}
if (failure) {
System.out.println("TESTS
FAILED!");
System.exit(1);
}
}
catch (Exception ex) {
System.out.println("ERROR: " + ex);
System.exit(1);
}
}
public static void
testHashFunctionChecking() {
try {
String
hash = PasswordEncryption.createHash("foobar");
hash
= hash.replaceFirst("sha1:", "sha256:");
boolean raised = false;
try {
PasswordEncryption.verifyPassword("foobar",
hash);
}
catch
(PasswordEncryption.CannotPerformOperationException ex) {
raised
= true;
}
if (raised) {
System.out.println("Algorithm swap:
pass");
}
else {
System.out.println("Algorithm swap:
FAIL");
System.exit(1);
}
}
catch (Exception e) {
System.err.println(e.getMessage());
System.exit(1);
}
}
}
Output:
Truncated hash test: pass
Algorithm
swap: pass
No comments:
Post a Comment