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 | } |