| 1 | /* |
| 2 | Copyright (C) 2002-2004 MySQL AB |
| 3 | |
| 4 | This program is free software; you can redistribute it and/or modify |
| 5 | it under the terms of version 2 of the GNU General Public License as |
| 6 | published by the Free Software Foundation. |
| 7 | |
| 8 | There are special exceptions to the terms and conditions of the GPL |
| 9 | as it is applied to this software. View the full text of the |
| 10 | exception in file EXCEPTIONS-CONNECTOR-J in the directory of this |
| 11 | software distribution. |
| 12 | |
| 13 | This program is distributed in the hope that it will be useful, |
| 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | GNU General Public License for more details. |
| 17 | |
| 18 | You should have received a copy of the GNU General Public License |
| 19 | along with this program; if not, write to the Free Software |
| 20 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 21 | |
| 22 | |
| 23 | |
| 24 | */ |
| 25 | package com.mysql.jdbc; |
| 26 | |
| 27 | import java.security.MessageDigest; |
| 28 | import java.security.NoSuchAlgorithmException; |
| 29 | |
| 30 | /** |
| 31 | * Methods for doing secure authentication with MySQL-4.1 and newer. |
| 32 | * |
| 33 | * @author Mark Matthews |
| 34 | * |
| 35 | * @version $Id: Security.java 3726 2005-05-19 15:52:24Z mmatthews $ |
| 36 | */ |
| 37 | class Security { |
| 38 | private static final char PVERSION41_CHAR = '*'; |
| 39 | |
| 40 | private static final int SHA1_HASH_SIZE = 20; |
| 41 | |
| 42 | /** |
| 43 | * Returns hex value for given char |
| 44 | */ |
| 45 | private static int charVal(char c) { |
| 46 | return ((c >= '0') && (c <= '9')) ? (c - '0') |
| 47 | : (((c >= 'A') && (c <= 'Z')) ? (c - 'A' + 10) : (c - 'a' + 10)); |
| 48 | } |
| 49 | |
| 50 | /* |
| 51 | * Convert password in salted form to binary string password and hash-salt |
| 52 | * For old password this involes one more hashing |
| 53 | * |
| 54 | * SYNOPSIS get_hash_and_password() salt IN Salt to convert from pversion IN |
| 55 | * Password version to use hash OUT Store zero ended hash here bin_password |
| 56 | * OUT Store binary password here (no zero at the end) |
| 57 | * |
| 58 | * RETURN 0 for pre 4.1 passwords !0 password version char for newer |
| 59 | * passwords |
| 60 | */ |
| 61 | |
| 62 | /** |
| 63 | * Creates key from old password to decode scramble Used in 4.1 |
| 64 | * authentication with passwords stored pre-4.1 hashing. |
| 65 | * |
| 66 | * @param passwd |
| 67 | * the password to create the key from |
| 68 | * |
| 69 | * @return 20 byte generated key |
| 70 | * |
| 71 | * @throws NoSuchAlgorithmException |
| 72 | * if the message digest 'SHA-1' is not available. |
| 73 | */ |
| 74 | static byte[] createKeyFromOldPassword(String passwd) |
| 75 | throws NoSuchAlgorithmException { |
| 76 | /* At first hash password to the string stored in password */ |
| 77 | passwd = makeScrambledPassword(passwd); |
| 78 | |
| 79 | /* Now convert it to the salt form */ |
| 80 | int[] salt = getSaltFromPassword(passwd); |
| 81 | |
| 82 | /* Finally get hash and bin password from salt */ |
| 83 | return getBinaryPassword(salt, false); |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * DOCUMENT ME! |
| 88 | * |
| 89 | * @param salt |
| 90 | * DOCUMENT ME! |
| 91 | * @param usingNewPasswords |
| 92 | * DOCUMENT ME! |
| 93 | * |
| 94 | * @return DOCUMENT ME! |
| 95 | * |
| 96 | * @throws NoSuchAlgorithmException |
| 97 | * if the message digest 'SHA-1' is not available. |
| 98 | */ |
| 99 | static byte[] getBinaryPassword(int[] salt, boolean usingNewPasswords) |
| 100 | throws NoSuchAlgorithmException { |
| 101 | int val = 0; |
| 102 | |
| 103 | byte[] binaryPassword = new byte[SHA1_HASH_SIZE]; /* |
| 104 | * Binary password |
| 105 | * loop pointer |
| 106 | */ |
| 107 | |
| 108 | if (usingNewPasswords) /* New password version assumed */{ |
| 109 | int pos = 0; |
| 110 | |
| 111 | for (int i = 0; i < 4; i++) /* Iterate over these elements */{ |
| 112 | val = salt[i]; |
| 113 | |
| 114 | for (int t = 3; t >= 0; t--) { |
| 115 | binaryPassword[pos++] = (byte) (val & 255); |
| 116 | val >>= 8; /* Scroll 8 bits to get next part */ |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | return binaryPassword; |
| 121 | } |
| 122 | |
| 123 | int offset = 0; |
| 124 | |
| 125 | for (int i = 0; i < 2; i++) /* Iterate over these elements */{ |
| 126 | val = salt[i]; |
| 127 | |
| 128 | for (int t = 3; t >= 0; t--) { |
| 129 | binaryPassword[t + offset] = (byte) (val % 256); |
| 130 | val >>= 8; /* Scroll 8 bits to get next part */ |
| 131 | } |
| 132 | |
| 133 | offset += 4; |
| 134 | } |
| 135 | |
| 136 | MessageDigest md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ |
| 137 | |
| 138 | md.update(binaryPassword, 0, 8); |
| 139 | |
| 140 | return md.digest(); |
| 141 | } |
| 142 | |
| 143 | private static int[] getSaltFromPassword(String password) { |
| 144 | int[] result = new int[6]; |
| 145 | |
| 146 | if ((password == null) || (password.length() == 0)) { |
| 147 | return result; |
| 148 | } |
| 149 | |
| 150 | if (password.charAt(0) == PVERSION41_CHAR) { |
| 151 | // new password |
| 152 | String saltInHex = password.substring(1, 5); |
| 153 | |
| 154 | int val = 0; |
| 155 | |
| 156 | for (int i = 0; i < 4; i++) { |
| 157 | val = (val << 4) + charVal(saltInHex.charAt(i)); |
| 158 | } |
| 159 | |
| 160 | return result; |
| 161 | } |
| 162 | |
| 163 | int resultPos = 0; |
| 164 | int pos = 0; |
| 165 | int length = password.length(); |
| 166 | |
| 167 | while (pos < length) { |
| 168 | int val = 0; |
| 169 | |
| 170 | for (int i = 0; i < 8; i++) { |
| 171 | val = (val << 4) + charVal(password.charAt(pos++)); |
| 172 | } |
| 173 | |
| 174 | result[resultPos++] = val; |
| 175 | } |
| 176 | |
| 177 | return result; |
| 178 | } |
| 179 | |
| 180 | private static String longToHex(long val) { |
| 181 | String longHex = Long.toHexString(val); |
| 182 | |
| 183 | int length = longHex.length(); |
| 184 | |
| 185 | if (length < 8) { |
| 186 | int padding = 8 - length; |
| 187 | StringBuffer buf = new StringBuffer(); |
| 188 | |
| 189 | for (int i = 0; i < padding; i++) { |
| 190 | buf.append("0"); //$NON-NLS-1$ |
| 191 | } |
| 192 | |
| 193 | buf.append(longHex); |
| 194 | |
| 195 | return buf.toString(); |
| 196 | } |
| 197 | |
| 198 | return longHex.substring(0, 8); |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Creates password to be stored in user database from raw string. |
| 203 | * |
| 204 | * Handles Pre-MySQL 4.1 passwords. |
| 205 | * |
| 206 | * @param password |
| 207 | * plaintext password |
| 208 | * |
| 209 | * @return scrambled password |
| 210 | * |
| 211 | * @throws NoSuchAlgorithmException |
| 212 | * if the message digest 'SHA-1' is not available. |
| 213 | */ |
| 214 | static String makeScrambledPassword(String password) |
| 215 | throws NoSuchAlgorithmException { |
| 216 | long[] passwordHash = Util.newHash(password); |
| 217 | StringBuffer scramble = new StringBuffer(); |
| 218 | |
| 219 | scramble.append(longToHex(passwordHash[0])); |
| 220 | scramble.append(longToHex(passwordHash[1])); |
| 221 | |
| 222 | return scramble.toString(); |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Encrypt/Decrypt function used for password encryption in authentication |
| 227 | * |
| 228 | * Simple XOR is used here but it is OK as we crypt random strings |
| 229 | * |
| 230 | * @param from |
| 231 | * IN Data for encryption |
| 232 | * @param to |
| 233 | * OUT Encrypt data to the buffer (may be the same) |
| 234 | * @param password |
| 235 | * IN Password used for encryption (same length) |
| 236 | * @param length |
| 237 | * IN Length of data to encrypt |
| 238 | */ |
| 239 | static void passwordCrypt(byte[] from, byte[] to, byte[] password, |
| 240 | int length) { |
| 241 | int pos = 0; |
| 242 | |
| 243 | while ((pos < from.length) && (pos < length)) { |
| 244 | to[pos] = (byte) (from[pos] ^ password[pos]); |
| 245 | pos++; |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Stage one password hashing, used in MySQL 4.1 password handling |
| 251 | * |
| 252 | * @param password |
| 253 | * plaintext password |
| 254 | * |
| 255 | * @return stage one hash of password |
| 256 | * |
| 257 | * @throws NoSuchAlgorithmException |
| 258 | * if the message digest 'SHA-1' is not available. |
| 259 | */ |
| 260 | static byte[] passwordHashStage1(String password) |
| 261 | throws NoSuchAlgorithmException { |
| 262 | MessageDigest md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ |
| 263 | StringBuffer cleansedPassword = new StringBuffer(); |
| 264 | |
| 265 | int passwordLength = password.length(); |
| 266 | |
| 267 | for (int i = 0; i < passwordLength; i++) { |
| 268 | char c = password.charAt(i); |
| 269 | |
| 270 | if ((c == ' ') || (c == '\t')) { |
| 271 | continue; /* skip space in password */ |
| 272 | } |
| 273 | |
| 274 | cleansedPassword.append(c); |
| 275 | } |
| 276 | |
| 277 | return md.digest(cleansedPassword.toString().getBytes()); |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Stage two password hashing used in MySQL 4.1 password handling |
| 282 | * |
| 283 | * @param hash |
| 284 | * from passwordHashStage1 |
| 285 | * @param salt |
| 286 | * salt used for stage two hashing |
| 287 | * |
| 288 | * @return result of stage two password hash |
| 289 | * |
| 290 | * @throws NoSuchAlgorithmException |
| 291 | * if the message digest 'SHA-1' is not available. |
| 292 | */ |
| 293 | static byte[] passwordHashStage2(byte[] hashedPassword, byte[] salt) |
| 294 | throws NoSuchAlgorithmException { |
| 295 | MessageDigest md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ |
| 296 | |
| 297 | // hash 4 bytes of salt |
| 298 | md.update(salt, 0, 4); |
| 299 | |
| 300 | md.update(hashedPassword, 0, SHA1_HASH_SIZE); |
| 301 | |
| 302 | return md.digest(); |
| 303 | } |
| 304 | |
| 305 | // SERVER: public_seed=create_random_string() |
| 306 | // send(public_seed) |
| 307 | // |
| 308 | // CLIENT: recv(public_seed) |
| 309 | // hash_stage1=sha1("password") |
| 310 | // hash_stage2=sha1(hash_stage1) |
| 311 | // reply=xor(hash_stage1, sha1(public_seed,hash_stage2) |
| 312 | // |
| 313 | // // this three steps are done in scramble() |
| 314 | // |
| 315 | // send(reply) |
| 316 | // |
| 317 | // |
| 318 | // SERVER: recv(reply) |
| 319 | // hash_stage1=xor(reply, sha1(public_seed,hash_stage2)) |
| 320 | // candidate_hash2=sha1(hash_stage1) |
| 321 | // check(candidate_hash2==hash_stage2) |
| 322 | static byte[] scramble411(String password, String seed) |
| 323 | throws NoSuchAlgorithmException { |
| 324 | MessageDigest md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ |
| 325 | |
| 326 | byte[] passwordHashStage1 = md.digest(password.getBytes()); |
| 327 | md.reset(); |
| 328 | |
| 329 | byte[] passwordHashStage2 = md.digest(passwordHashStage1); |
| 330 | md.reset(); |
| 331 | |
| 332 | byte[] seedAsBytes = seed.getBytes(); // for debugging |
| 333 | md.update(seedAsBytes); |
| 334 | md.update(passwordHashStage2); |
| 335 | |
| 336 | byte[] toBeXord = md.digest(); |
| 337 | |
| 338 | int numToXor = toBeXord.length; |
| 339 | |
| 340 | for (int i = 0; i < numToXor; i++) { |
| 341 | toBeXord[i] = (byte) (toBeXord[i] ^ passwordHashStage1[i]); |
| 342 | } |
| 343 | |
| 344 | return toBeXord; |
| 345 | } |
| 346 | |
| 347 | /** |
| 348 | * Prevent construction. |
| 349 | */ |
| 350 | private Security() { |
| 351 | super(); |
| 352 | } |
| 353 | } |