/*
* LoginKeyGenerator.java
*
* Created on May 12, 2007, 6:12 PM
*************************************************************************
* Copyright 2008 Paul Smith
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ao.protocol;
import ao.misc.Convert;
import java.math.BigInteger;
import java.util.Random;
/**
* <p>AOLoginKeyGenerator is a utilty class for generating the encrypted login keys
* that are used to authenticate a bot/user. The algorithm involves
* Diffie-Hellman Key Exchange, Tiny Encryption Algorithm (TEA),
* and likely some other unidentified algorithms.</p>
*
* <p><b>References:</b>
* <ul>
* <li><a href="http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange" target="_blank">Diffie-Hellman Key Exchange</a></li>
* <li><a href="http://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm" target="_blank">Tiny Encryption Algorithm (TEA)</a></li>
* <li><a href="http://boards.hackersquest.org/viewtopic.php?p=33507" target="_blank">Class ao.f from JavaBot</a></li>
* <li><a href="http://sourceforge.net/projects/aochat" target="_blank">Class aochat.loginMessage from a J++ based bot library written by migisan and quaseem</a></li>
* <li><a href="http://kiwu.org/aochat/" target="_blank">Script key.py from a Python port of AOChat.php</a></li>
* <li><a href="http://auno.org/dev/aochat/AOChat.php" target="_blank">AOChat.php</a></li>
* </ul></p>
*
* @see #generateLoginKey(String, String, String)
* @see ao.protocol.packets.toclient.LoginSeedPacket
* @see ao.protocol.packets.toserver.LoginRequestPacket
* @see ao.protocol.Client#authenticate(String, String)
*/
public class LoginKeyGenerator {
/**
* A constant that is sent to the AO server when attempting to authenticate a bot/user.
* @see ao.protocol.packets.toserver.LoginRequestPacket
*/
public static int PROTOCOL_VERSION = 0;
/** Used to generate random numbers whenever necessary. */
private static Random random = new Random();
/** See <a href="http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange">Diffie-Hellman Key Exchange</a> */
private static BigInteger P = new BigInteger( "eca2e8c85d863dcdc26a429a71a9815ad052f6139669dd659f98ae159d313d13c6bf2838e10a69b6478b64a24bd054ba8248e8fa778703b418408249440b2c1edd28853e240d8a7e49540b76d120d3b1ad2878b1b99490eb4a2a5e84caa8a91cecbdb1aa7c816e8be343246f80c637abc653b893fd91686cf8d32d6cfe5f2a6f", 16 );
/** See <a href="http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange">Diffie-Hellman Key Exchange</a> */
private static BigInteger G1 = new BigInteger( "9c32cc23d559ca90fc31be72df817d0e124769e809f936bc14360ff4bed758f260a0d596584eacbbc2b88bdd410416163e11dbf62173393fbc0c6fefb2d855f1a03dec8e9f105bbad91b3437d8eb73fe2f44159597aa4053cf788d2f9d7012fb8d7c4ce3876f7d6cd5d0c31754f4cd96166708641958de54a6def5657b9f2e92", 16 );
/** See <a href="http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange">Diffie-Hellman Key Exchange</a> */
private static BigInteger G2 = new BigInteger( "5", 16 );
/**
* Given a server seed and a user name and password pair,
* this function generates an encrypted login key.
*
* @param serverSeed
* the seed that the AO server provided when the connection to it was first opened
* (this is currently a 32 character hexadecimal string (128 bit))
* @param accountName
* the user name that the bot/user provided
* @param password
* the password that the bot/user provided
* @return
* the generated encrypted login key
* @throws NullPointerException
* if serverSeed, userName, or password are null
*
* @see ao.protocol.packets.toclient.LoginSeedPacket
* @see ao.protocol.packets.toserver.LoginRequestPacket
* @see ao.protocol.Client#authenticate(String, String)
*/
public static String generateLoginKey(String serverSeed, String accountName, String password) {
if (serverSeed == null) { throw new NullPointerException("No server seed was given."); }
else if (accountName == null) { throw new NullPointerException("No user name was given."); }
else if (password == null) { throw new NullPointerException("No password was given."); }
else {
return generateLoginKey( serverSeed, generateLocalSeed(16), accountName, password, P, G1, G2);
} // end else
} // end generateLoginKey()
/**
* Given a server seed, a local seed, and a user name and password pair,
* this function generates an encrypted login key.
*
* @param serverSeed
* the seed that the AO server provided when the connection to it was first opened
* (this is currently a 32 character hexadecimal string (128 bit))
* @param localSeed
* a random 16 character hexadecimal string (64 bit) generated by the client
* @param accountName
* the user name that the bot/user provided
* @param password
* the password that the bot/user provided
* @return
* the generated encrypted login key
*
* @see ao.protocol.packets.toclient.LoginSeedPacket
* @see ao.protocol.packets.toclient.LoginRequestPacket
* @see ao.protocol.Client#authenticate(String, String)
* @see #generateLocalSeed(int)
* @see #generateLoginKey(String, String, String)
* @see <a href="http://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange">Diffie-Hellman Key Exchange</a>
*/
private static String generateLoginKey(String serverSeed, String localSeed, String accountName, String password, BigInteger prime, BigInteger g1, BigInteger g2) {
BigInteger bigLocalSeed = new BigInteger(localSeed, 16);
BigInteger bigDecryptionKey = g2.modPow(bigLocalSeed, prime);
BigInteger bigEncyrptionKey = g1.modPow(bigLocalSeed, prime);
String encryptionKey = truncateEncryptionKey(bigEncyrptionKey.toString(16), 32);
String loginData = accountName + "|" + serverSeed + "|" + password;
String encryptedLoginData = encrypt(encryptionKey, loginData);
return bigDecryptionKey.toString(16) + "-" + encryptedLoginData;
} // end generateLoginKey()
/**
* Generates a random local seed.
* The seed generated is a {@code length} character hexadecimal string ({@code length*4} bit).
*
* @param length
* the number of characters in the generated seed
* @return
* a random {@code length} character hexadecimal string ({@code length*4} bit)
*
* @see #generateLoginKey(String, String, String, String)
*/
private static String generateLocalSeed(int length) {
String seed = "";
for (int i = 0; i < length; ++i) {
seed += Integer.toHexString( random.nextInt(8) );
} // end for
return seed;
} // end generateLocalSeed()
/**
* Truncates or zero fills the encryption key
* so that it is always {@code length} characters long.
* If {@code key.length()} is greater than {@code length},
* then the first {@code length} characters of the key are returned.
* If <code>key.length()</code> is less than {@code length},
* then {@code length - key.length()} zeros are inserted
* at the begining of the key and the result is returned.
*
* @param key
* the encryption key that will be truncated or zero filled
* @param length
* the length of the returned encryption key
* @return
* the truncated or zero-filled encryption key
*
* @see #generateLoginKey(String, String, String, String)
* @see #encrypt(String, String)
*/
private static String truncateEncryptionKey(String key, int length) {
if (key.length() > length) {
return key.substring(0, length);
} else {
while (key.length() < length) {
key = "0" + key;
} // end while
return key;
} // end else
} // end truncateEncryptionKey()
/**
* Encrypts a bot/user's login data using a 128 bit encryption key
* (passed as a 32 character hexadecimal string).
*
* @param encryptionKey
* the 128 bit encryption key used in the encryption process
* (passed as a 32 character hexadecimal string)
* @param loginData
* the login data that will be encrypted
* @return
* the encrypted login data
*/
public static String encrypt(String encryptionKey, String loginData) {
if (encryptionKey.length() < 32) { throw new IllegalArgumentException("The encryption key is too short."); }
String prefix = generatePrefix(8);
String loginDataLength = loginDataLengthToString(loginData.length());
String pad = generatePad( prefix.length() + loginDataLength.length() + loginData.length() );
String unencrypted = prefix + loginDataLength + loginData + pad;
String encrypted = "";
int[] encryptionKeyInts = Convert.hexStringToIntArray(encryptionKey);
int[] unencryptedInts = Convert.stringToIntArray(unencrypted);
int[] oldBlock = { 0, 0 };
int[] newBlock = { 0, 0 };
for (int i = 0; i < unencryptedInts.length; i += 2) {
newBlock[0] = unencryptedInts[i];
newBlock[1] = unencryptedInts[i + 1];
if (i != 0) {
newBlock[0] ^= oldBlock[0];
newBlock[1] ^= oldBlock[1];
} // end if
Tea.encrypt(newBlock, encryptionKeyInts);
oldBlock[0] = newBlock[0];
oldBlock[1] = newBlock[1];
encrypted += Convert.intArrayToHexString(newBlock);
} // end for
return encrypted;
} // end encrypt()
/**
* Generates a random sequence of {@code length} characters.
*
* @param length
* the number of characters in the generated string
* @return
* a random sequence of {@code length} characters
*
* @see #encrypt(String, String)
*/
private static String generatePrefix(int length) {
char[] chars = new char[length];
for (int i = 0; i < length; ++i) {
chars[i] = (char)random.nextInt(255);
} // end for
return new String(chars);
} // end generatePrefix()
/**
* Turns the length of the actual login data into a sequence of characters.
*
* @param length
* the length of the actual login data
* @return
* a four character string created from the given integer's binary data
*
* @see #encrypt(String, String)
*/
private static String loginDataLengthToString(int length) {
char chars[] = new char[4];
chars[0] = (char)(length >>> 24 );
chars[1] = (char)(length >>> 16 & 0xff);
chars[2] = (char)(length >>> 8 & 0xff);
chars[3] = (char)(length & 0xff);
return new String(chars);
} // end loginDataLengthToString()
/**
* Creates a sequence of ' ' characters to append
* to the unencrypted login data, so that the length
* of the unecrypted login data is evenly divisible by 8.
*
* @param totalLength
* the length of the unecrypted login data (without the pad)
* @return
* a sequence of ' ' characters
*
* @see #encrypt(String, String)
* @see #tea(int[], int[])
*/
private static String generatePad(int totalLength) {
String pad = "";
int length = 8 - totalLength % 8;
if (length < 8) {
for (int i = 0; i < length; ++i) {
pad += " ";
} // end for
} // end if
return pad;
} // end generatePad()
} // end class LoginKeyGenerator