/*
* Hamsam - Instant Messaging API
* Copyright (C) 2003 Raghu K
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package hamsam.protocol.yahoo;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
class Crypt {
/**
* Encryption method indicator
*/
private int method;
private static final char[] base64Digits = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '.', '_' };
private String yahooId, password;
char[] seed;
public Crypt(String yahooId, String password, String seed)
throws NullPointerException {
if (seed == null)
throw new NullPointerException("seed is null");
if (yahooId == null)
throw new NullPointerException("yahooId is null");
this.yahooId = new String(yahooId);
this.password = new String(password);
this.seed = seed.toCharArray();
}
public String[] doEncrypt() {
try {
if (this.method == 1)
return do0x0bEncrypt();
else
return doPre0x0bEncrypt();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
/**
* The new encryption introduced in 0x0b protocol. Thanks to Gaim
* developers for cracking this so fast.
*/
private String[] do0x0bEncrypt() throws NoSuchAlgorithmException {
char[] magic = new char[64];
int magicLength = do0x0bMagic1(magic);
do0x0bMagic2(magicLength, magic);
byte[] magicKeyChar = do0x0bMagic3(magicLength, magic);
// Get password and crypt hashes as per usual.
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(password.getBytes());
char[] result = Util.byteArrayToCharArray(md5.digest());
char[] passwordHash = toBase64(result);
md5.reset();
byte[] cryptResult = crypt(password, "$1$_2S43d5f$");
md5.update(cryptResult);
result = Util.byteArrayToCharArray(md5.digest());
char[] cryptHash = toBase64(result);
// Our first authentication response is based off the password hash.
String resp6 = get0x0bResponseString(passwordHash, magicKeyChar);
// Our second authentication response is based off the crypto hash.
String resp96 = get0x0bResponseString(cryptHash, magicKeyChar);
String ret[] = new String[2];
ret[0] = resp6.toString();
ret[1] = resp96.toString();
return ret;
}
/*
* Magic: Phase 1. Generate what seems to be a 30
* byte value (could change if base64
* ends up differently? I don't remember and I'm
* tired, so use a 64 byte buffer.
*/
private int do0x0bMagic1(char[] magic) {
String challengeLookup = "qzec2tb3um1olpar8whx4dfgijknsvy5";
String operandLookup = "+|&%/*^-";
int magicCount = 0;
int magicWork = 0;
for (int magicPtr = 0; magicPtr < seed.length; magicPtr++) {
// Ignore parentheses.
if (seed[magicPtr] == '(' || seed[magicPtr] == ')')
continue;
// Characters and digits verify against the challenge lookup.
if (Character.isLetterOrDigit(seed[magicPtr])) {
int loc = challengeLookup.indexOf(seed[magicPtr]);
if (loc == -1) {
// This isn't good
continue;
}
// Get offset into lookup table and lsh 3.
magicWork = loc << 3;
continue;
} else {
int loc = operandLookup.indexOf(seed[magicPtr]);
if (loc == -1) {
// Also not good.
continue;
}
// Oops; how did this happen?
if (magicCount >= 64)
break;
magic[magicCount++] = (char) (magicWork | loc);
continue;
}
}
return magicCount;
}
/* Magic: Phase 2. Take generated magic value and sprinkle fairy dust on the values. */
private void do0x0bMagic2(int magicLength, char[] magic) {
for (int magicCount = magicLength - 2; magicCount >= 0; magicCount--) {
char byte1;
char byte2;
// Bad. Abort.
if (magicCount >= magicLength)
break;
byte1 = magic[magicCount];
byte2 = magic[magicCount + 1];
byte1 *= 0xcd;
byte1 ^= byte2;
magic[magicCount + 1] = byte1;
}
}
/* Magic: Phase 3. Final phase; this gives us our key. */
private byte[] do0x0bMagic3(int magicLength, char[] magic)
throws NoSuchAlgorithmException {
int magicCount = 1;
int dump[] = new int[20];
for (int index = 0; index < 20;) {
int bl = 0;
int cl = magic[magicCount++];
if (magicCount >= magicLength)
break;
if (cl > 0x7F) {
if (cl < 0xe0)
bl = cl = (cl & 0x1f) << 6;
else {
bl = magic[magicCount++];
cl = (cl & 0x0f) << 6;
bl = ((bl & 0x3f) + cl) << 6;
}
cl = magic[magicCount++];
bl = (cl & 0x3f) + bl;
} else
bl = cl;
dump[index++] = (bl & 0xff00) >>> 8;
dump[index++] = bl & 0xff;
}
// First four bytes are magic key.
byte[] chal = new byte[7];
byte magicKeyChar[] = new byte[4];
for (int i = 0; i < 4; i++)
chal[i] = magicKeyChar[i] = (byte) dump[i];
// Compute values for recursive function table!
boolean done = false;
byte compare[] = new byte[16];
for (int i = 0; i < 16; i++)
compare[i] = (byte) dump[i + 4];
int table = 0;
int depth = 0;
for (int i = 0; i < 0xffff && !done; i++) {
for (int j = 0; j < 5 && !done; j++) {
chal[4] = (byte) i;
chal[5] = (byte) (i >> 8);
chal[6] = (byte) j;
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(chal);
byte[] result = md5.digest();
if (Arrays.equals(compare, result)) {
depth = i;
table = j;
done = true;
}
}
}
// Transform magicKeyChar using transform table
int value = convertToInt(magicKeyChar);
value = YahooTransformation.transfrom(table, depth, value);
value = YahooTransformation.transfrom(table, depth, value);
magicKeyChar[0] = (byte) (value & 0xff);
magicKeyChar[1] = (byte) ((value >> 8) & 0xff);
magicKeyChar[2] = (byte) ((value >> 16) & 0xff);
magicKeyChar[3] = (byte) ((value >> 24) & 0xff);
return magicKeyChar;
}
private int convertToInt(byte[] bytes) {
long ret = 0;
for (int i = 3; i >= 0; i--) {
ret <<= 8;
if (bytes[i] >= 0)
ret |= bytes[i];
else
ret |= (256 + bytes[i]);
}
return (int) ret;
}
private String get0x0bResponseString(char[] hash, byte[] magicKeyChar)
throws NoSuchAlgorithmException {
byte[] hashXOR1 = new byte[64];
byte[] hashXOR2 = new byte[64];
int x, cnt = 0;
for (x = 0; x < hash.length; x++)
hashXOR1[cnt++] = (byte) (hash[x] ^ 0x36);
for (x = cnt; x < hashXOR1.length; x++)
hashXOR1[x] = 0x36;
cnt = 0;
for (x = 0; x < hash.length; x++)
hashXOR2[cnt++] = (byte) (hash[x] ^ 0x5c);
for (x = cnt; x < hashXOR2.length; x++)
hashXOR2[x] = 0x5c;
MessageDigest sha1 = MessageDigest.getInstance("SHA");
MessageDigest sha2 = MessageDigest.getInstance("SHA");
/* The first context gets the password hash XORed with 0x36 plus a magic
* value which we previously extrapolated from our challenge. */
sha1.update(hashXOR1);
sha1.update(magicKeyChar);
byte[] digest1 = sha1.digest();
/* The second context gets the password hash XORed
* with 0x5c plus the SHA-1 digest of the first context. */
sha2.update(hashXOR2);
sha2.update(digest1);
char[] digest2 = Util.byteArrayToCharArray(sha2.digest());
/* Now that we have digest2, use it to fetch characters from
* an alphabet to construct our first authentication response. */
char[] alphabet1 = "FBZDWAGHrJTLMNOPpRSKUVEXYChImkwQ".toCharArray();
char[] alphabet2 = "F0E1D2C3B4A59687abcdefghijklmnop".toCharArray();
char[] delimitLookup = ",;".toCharArray();
StringBuffer response = new StringBuffer();
for (x = 0; x < 20; x += 2) {
int val = 0;
int lookup = 0;
// First two bytes of digest stuffed together.
val = digest2[x];
val <<= 8;
val += digest2[x + 1] & 0xff;
lookup = (val >> 0x0b);
lookup &= 0x1f;
if (lookup >= alphabet1.length)
break;
response.append(alphabet1[lookup]);
response.append('=');
lookup = (val >> 0x06);
lookup &= 0x1f;
if (lookup >= alphabet2.length)
break;
response.append(alphabet2[lookup]);
lookup = (val >> 0x01);
lookup &= 0x1f;
if (lookup >= alphabet2.length)
break;
response.append(alphabet2[lookup]);
lookup = (val & 0x01);
if (lookup >= delimitLookup.length)
break;
response.append(delimitLookup[lookup]);
}
return response.toString();
}
private String[] doPre0x0bEncrypt() throws NoSuchAlgorithmException {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(password.getBytes());
char[] result = Util.byteArrayToCharArray(md5.digest());
char[] passwordHash = toBase64(result);
md5.reset();
byte[] cryptResult = crypt(password, "$1$_2S43d5f$");
md5.update(cryptResult);
result = Util.byteArrayToCharArray(md5.digest());
char[] cryptHash = toBase64(result);
int sv = seed[15] % 8;
char checksum = 0;
String hashStringP = null, hashStringC = null;
switch (sv % 5) {
case 0 :
checksum = (char) (seed[seed[7] % 16] & 0xff);
hashStringP =
String.valueOf(checksum)
+ new String(passwordHash)
+ yahooId
+ new String(seed);
hashStringC =
String.valueOf(checksum)
+ new String(cryptHash)
+ yahooId
+ new String(seed);
break;
case 1 :
checksum = (char) (seed[seed[9] % 16] & 0xff);
hashStringP =
String.valueOf(checksum)
+ yahooId
+ new String(seed)
+ new String(passwordHash);
hashStringC =
String.valueOf(checksum)
+ yahooId
+ new String(seed)
+ new String(cryptHash);
break;
case 2 :
checksum = (char) (seed[seed[15] % 16] & 0xff);
hashStringP =
String.valueOf(checksum)
+ new String(seed)
+ new String(passwordHash)
+ yahooId;
hashStringC =
String.valueOf(checksum)
+ new String(seed)
+ new String(cryptHash)
+ yahooId;
break;
case 3 :
checksum = (char) (seed[seed[1] % 16] & 0xff);
hashStringP =
String.valueOf(checksum)
+ yahooId
+ new String(passwordHash)
+ new String(seed);
hashStringC =
String.valueOf(checksum)
+ yahooId
+ new String(cryptHash)
+ new String(seed);
break;
case 4 :
checksum = (char) (seed[seed[3] % 16] & 0xff);
hashStringP =
String.valueOf(checksum)
+ new String(passwordHash)
+ new String(seed)
+ yahooId;
hashStringC =
String.valueOf(checksum)
+ new String(cryptHash)
+ new String(seed)
+ yahooId;
break;
}
md5.reset();
md5.update(hashStringP.getBytes());
result = Util.byteArrayToCharArray(md5.digest());
char[] result6 = toBase64(result);
md5.reset();
md5.update(hashStringC.getBytes());
result = Util.byteArrayToCharArray(md5.digest());
char[] result96 = toBase64(result);
String ret[] = new String[2];
ret[0] = new String(result6);
ret[1] = new String(result96);
return ret;
}
private char[] toBase64(char[] in) {
int len = in.length;
char[] out = new char[24];
int index = 0;
int i = 0;
for (; len >= 3; len -= 3) {
out[index++] = base64Digits[in[i] >>> 2];
out[index++] =
base64Digits[((in[i] << 4) & 0x30) | (in[i + 1] >>> 4)];
out[index++] =
base64Digits[((in[i + 1] << 2) & 0x3c) | (in[i + 2] >>> 6)];
out[index++] = base64Digits[in[i + 2] & 0x3f];
i += 3;
}
if (len > 0) {
char fragment;
out[index++] = base64Digits[in[i] >>> 2];
fragment = (char) ((in[i] << 4) & 0x30);
if (len > 1)
fragment |= in[i + 1] >>> 4;
out[index++] = base64Digits[fragment];
out[index++] =
(len < 2) ? '-' : base64Digits[(in[i + 1] << 2) & 0x3c];
out[index++] = '-';
}
return out;
}
private byte[] crypt(String key, String salt)
throws NoSuchAlgorithmException {
String md5SaltPrefix = "$1$";
// Find beginning of salt string. The prefix should normally always
// be present. Just in case it is not.
if (salt.startsWith(md5SaltPrefix))
// Skip salt prefix.
salt = salt.substring(md5SaltPrefix.length());
int saltLen = salt.indexOf('$');
if (saltLen == -1)
saltLen = salt.length();
if (saltLen > 8)
saltLen = 8;
// Prepare for the real work.
MessageDigest md1 = MessageDigest.getInstance("MD5");
// Add the key string.
md1.update(key.getBytes());
// Because the SALT argument need not always have the salt prefix we
// add it separately.
md1.update(md5SaltPrefix.getBytes());
// The last part is the salt string. This must be at most 8
// characters and it ends at the first `$' character (for
// compatibility which existing solutions).
md1.update(salt.getBytes(), 0, saltLen);
/* Compute alternate MD5 sum with input KEY, SALT, and KEY. The
final result will be added to the first context. */
MessageDigest md2 = MessageDigest.getInstance("MD5");
// Add key.
md2.update(key.getBytes());
// Add salt.
md2.update(salt.getBytes(), 0, saltLen);
// Add key again.
md2.update(key.getBytes());
// Now get result of this (16 bytes) and add it to the other context.
byte[] altResult = md2.digest();
// Add for any character in the key one byte of the alternate sum.
int cnt;
for (cnt = key.length(); cnt > 16; cnt -= 16)
md1.update(altResult, 0, 16);
md1.update(altResult, 0, cnt);
// For the following code we need a NUL byte.
altResult[0] = 0;
/* The original implementation now does something weird: for every 1
bit in the key the first 0 is added to the buffer, for every 0
bit the first character of the key. This does not seem to be
what was intended but we have to follow this to be compatible. */
for (cnt = key.length(); cnt > 0; cnt >>= 1) {
if ((cnt & 1) != 0)
md1.update(altResult, 0, 1);
else
md1.update(key.getBytes(), 0, 1);
}
// Create intermediate result.
altResult = md1.digest();
/* Now comes another weirdness. In fear of password crackers here
comes a quite long loop which just processes the output of the
previous round again. We cannot ignore this here. */
for (cnt = 0; cnt < 1000; ++cnt) {
// New context.
md1.reset();
// Add key or last result.
if ((cnt & 1) != 0)
md1.update(key.getBytes());
else
md1.update(altResult, 0, 16);
// Add salt for numbers not divisible by 3.
if (cnt % 3 != 0)
md1.update(salt.getBytes(), 0, saltLen);
// Add key for numbers not divisible by 7.
if (cnt % 7 != 0)
md1.update(key.getBytes());
// Add key or last result.
if ((cnt & 1) != 0)
md1.update(altResult, 0, 16);
else
md1.update(key.getBytes());
// Create intermediate result.
altResult = md1.digest();
}
// Now we can construct the result string. It consists of three parts.
StringBuffer buffer = new StringBuffer();
buffer.append(md5SaltPrefix);
buffer.append(salt);
buffer.append(
b64From24Bit(altResult[0], altResult[6], altResult[12], 4));
buffer.append(
b64From24Bit(altResult[1], altResult[7], altResult[13], 4));
buffer.append(
b64From24Bit(altResult[2], altResult[8], altResult[14], 4));
buffer.append(
b64From24Bit(altResult[3], altResult[9], altResult[15], 4));
buffer.append(
b64From24Bit(altResult[4], altResult[10], altResult[5], 4));
buffer.append(b64From24Bit((byte) 0, (byte) 0, altResult[11], 2));
return buffer.toString().getBytes();
}
private String b64From24Bit(byte b2, byte b1, byte b0, int count) {
int i2 = b2 >= 0 ? b2 : 256 + b2;
int i1 = b1 >= 0 ? b1 : 256 + b1;
int i0 = b0 >= 0 ? b0 : 256 + b0;
char[] b64t =
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
.toCharArray();
String ret = new String();
int w = (i2 << 16) | (i1 << 8) | i0;
while (count-- > 0) {
ret += b64t[w & 0x3f];
w >>>= 6;
}
return ret;
}
/**
* Set the encryption method used by Yahoo. If method is 1, we will use the
* new method as in version 0x0b, otherwise the old one will be used.
*
* @param method 1 indicates new authentication of protocol 0x0b, all other
* values indicates old encryption.
*/
public void setMethod(int method) {
this.method = method;
}
}