// Copyright 2005 Nick Mathewson, Roger Dingledine // See LICENSE file for copying information package org.torproject.jtor.control.auth; import java.security.SecureRandom; import java.security.MessageDigest; /** * A hashed digest of a secret password (used to set control connection * security.) * * For the actual hashing algorithm, see RFC2440's secret-to-key conversion. */ public class PasswordDigest { byte[] secret; String hashedKey; /** Return a new password digest with a random secret and salt. */ public static PasswordDigest generateDigest() { byte[] secret = new byte[20]; SecureRandom rng = new SecureRandom(); rng.nextBytes(secret); return new PasswordDigest(secret); } /** Construct a new password digest with a given secret as it may appear in torrc */ public PasswordDigest(String in) { byte[] specifier; if (in.startsWith("16:")) { hashedKey = in; } else { secret = removeQuotes(in).getBytes(); specifier = new byte[9]; SecureRandom rng = new SecureRandom(); rng.nextBytes(specifier); specifier[8] = 96; hashedKey = "16:"+encodeBytes(secretToKey(secret, specifier)); } } /** Construct a new password digest with a given secret and random salt */ public PasswordDigest(byte[] secret) { this(secret, null); } /** Construct a new password digest with a given secret and random salt. * Note that the 9th byte of the specifier determines the number of hash * iterations as in RFC2440. */ public PasswordDigest(byte[] secret, byte[] specifier) { this.secret = secret.clone(); if (specifier == null) { specifier = new byte[9]; SecureRandom rng = new SecureRandom(); rng.nextBytes(specifier); specifier[8] = 96; } hashedKey = "16:"+encodeBytes(secretToKey(secret, specifier)); } /** Return the secret used to generate this password hash. */ public byte[] getSecret() { return secret.clone(); } /** Return the hashed password in the format used by Tor. */ public String getHashedPassword() { return hashedKey; } /** Verifies if a key matches the set password, may be plain text or hashed. */ public boolean verifyPassword(String in) { if (in.startsWith("16:")) { // hashed password if (secret == null) { // can't authenticate this without the secret return false; } byte[] salt = hexStringToByteArray(in.substring(3, 21)); PasswordDigest pwd = new PasswordDigest(secret, salt); return pwd.getHashedPassword().equals(in); } else if (in.startsWith("\"") && in.endsWith("\"")) { // plain text password byte[] salt = hexStringToByteArray(hashedKey.substring(3, 21)); byte[] key = in.substring(1, in.length()-1).getBytes(); PasswordDigest pwd = new PasswordDigest(key, salt); return hashedKey.equals(pwd.getHashedPassword()); } else { // hex encoded string byte[] salt = hexStringToByteArray(hashedKey.substring(3, 21)); byte[] key = hexStringToByteArray(in); PasswordDigest pwd = new PasswordDigest(key, salt); return hashedKey.equals(pwd.getHashedPassword()); } } /** Parameter used by RFC2440's s2k algorithm. */ private static final int EXPBIAS = 6; /** Implement rfc2440 s2k */ public static byte[] secretToKey(byte[] secret, byte[] specifier) { MessageDigest d; try { d = MessageDigest.getInstance("SHA-1"); } catch (java.security.NoSuchAlgorithmException ex) { throw new RuntimeException("Can't run without sha-1."); } int c = (specifier[8])&0xff; int count = (16 + (c&15)) << ((c>>4) + EXPBIAS); byte[] tmp = new byte[8+secret.length]; System.arraycopy(specifier, 0, tmp, 0, 8); System.arraycopy(secret, 0, tmp, 8, secret.length); while (count > 0) { if (count >= tmp.length) { d.update(tmp); count -= tmp.length; } else { d.update(tmp, 0, count); count = 0; } } byte[] key = new byte[20+9]; System.arraycopy(d.digest(), 0, key, 9, 20); System.arraycopy(specifier, 0, key, 0, 9); return key; } /** Return a hexadecimal encoding of a byte array. */ // XXX There must be a better way to do this in Java. private static final String encodeBytes(byte[] ba) { char[] NYBBLES = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; StringBuffer buf = new StringBuffer(); for (int i = 0; i < ba.length; ++i) { int b = (ba[i]) & 0xff; buf.append(NYBBLES[b >> 4]); buf.append(NYBBLES[b&0x0f]); } return buf.toString(); } public static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } return data; } /** Removes any unescaped quotes from a given string */ private String removeQuotes(String in) { int index = in.indexOf("\""); while (index < in.length() && index > 0) { if (!in.substring(index-1, index).equals("\\")) { //remove the quote as it's not escaped in = in.substring(0, index) + in.substring(index+1); } index = in.indexOf("\"", index); } return in; } }