/* * pwhash - a password scrambling library * Copyright (c) $year Noa Resare * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.voxbiblia.pwhash; import java.io.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; /** * This class converts an end user supplied password into a hash value that can * be used to verify if a specific value matches the original password or not. * * If the underlying operating system provides a high entropy source of * randomness for the salt values via the /dev/urandom device file it is used, * else the standard java Random class is used. * */ public class PasswordHasher { final Random random; InputStream osRandom; static ThreadLocal digestHolder = new ThreadLocal(); private static final String DIGEST_ALGORITHM = "SHA"; private static final int ITERATIONS = 1024; public PasswordHasher() { File f = new File("/dev/urandom"); if (f.exists()) { random = new Random(); osRandom = null; } else { random = null; try { osRandom = new FileInputStream(f); } catch (FileNotFoundException e) { throw new Error(e); } } } /** * Converts a plaintext password into hashed form suitable for persistent * storage. * * @param plaintext in cleartext * @return a password hash */ public String hash(String plaintext) { if (plaintext == null) { throw new IllegalArgumentException("Can't hash null plaintext"); } MessageDigest md = getMessageDigest(); byte[] salt = getSalt(); md.update(salt); try { md.update(plaintext.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new Error(e); } byte[] b0 = iterate(md.digest(), ITERATIONS); byte[] b1 = new byte[b0.length + 6]; // format version. In case we forget something. b1[0] = (byte)1; // the salt System.arraycopy(salt, 0, b1, 1, 4); // the iteration count b1[5] = iterationsByte(ITERATIONS); // the hash System.arraycopy(b0, 0, b1, 6, b0.length); return Base64.encode(b1); } /** * Verifies that the plaintext password matches the supplied password hash * * * @param plaintext the password to verify * @param hash a pre-computed password hash created using the hash method * @return true if the plaintext matches the hash */ public static boolean verify(String plaintext, String hash) { if (plaintext == null || hash == null) { throw new IllegalArgumentException("Null plaintext or hash"); } byte[] b0 = Base64.decode(hash); if (b0[0] != (byte)1) { throw new IllegalArgumentException("Illegal hash format, " + "expected 1: "+ b0[0]); } MessageDigest md = getMessageDigest(); md.update(b0, 1, 4); try { md.update(plaintext.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new Error(e); } int iterations = b0[5] * 128; byte[] b1 = iterate(md.digest(), iterations); for (int i = 0 ; i < b0.length - 6; i++) { if (b0[i + 6] != b1[i]) { return false; } } return true; } static byte iterationsByte(int iterations) { if (iterations % 128 != 0) { throw new Error("the iterations value needs to be evenly " + "dividable with 128"); } if (iterations > 128 * Byte.MAX_VALUE) { throw new Error("iterations value can not be higher than " + 128 * Byte.MAX_VALUE); } return (byte)(iterations / 128); } static byte[] iterate(byte[] initialValue, int iterations) { while(iterations-- > 0) { MessageDigest md = getMessageDigest(); md.update(initialValue); initialValue = md.digest(); } return initialValue; } /** * Returns four random bytes, with as much entropy as possible. * * @return four random bytes. */ private byte[] getSalt() { byte[] b = new byte[4]; if (osRandom != null) { try { //noinspection ResultOfMethodCallIgnored if (osRandom.read(b) != 4) { throw new Error("short read from random device"); } } catch (IOException e) { throw new Error(e); } } else { synchronized (random) { random.nextBytes(b); } } return b; } private static MessageDigest getMessageDigest() { MessageDigest md = digestHolder.get(); if (md == null) { try { md = MessageDigest.getInstance(DIGEST_ALGORITHM); digestHolder.set(md); return md; } catch (NoSuchAlgorithmException e) { throw new Error(e); } } return md; } }