TeaUtil.java:java
import java.io.ByteArrayOutputStream;
import java.util.Random;
/**
* 加密解密QQ消息的工具類. QQ消息的加密算法是一個16次的迭代過程,而且是反饋的,每個加密單元是8字節,輸出也是8字節,密鑰是16字節
* 咱們以prePlain表示前一個明文塊,plain表示當前明文塊,crypt表示當前明文塊加密獲得的密文塊,preCrypt表示前一個密文塊
* f表示加密算法,d表示解密算法 那麼從plain獲得crypt的過程是: crypt = f(plain ˆ preCrypt) ˆ
* prePlain 因此,從crypt獲得plain的過程天然是 plain = d(crypt ˆ prePlain) ˆ
* preCrypt 此外,算法有它的填充機制,其會在明文前和明文後分別填充必定的字節數,以保證實文長度是8字節的倍數
* 填充的字節數與原始明文長度有關,填充的方法是:
*
* <pre>
* <code>
*
* ------- 消息填充算法 -----------
* a = (明文長度 + 10) mod 8
* if(a 不等於 0) a = 8 - a;
* b = 隨機數 & 0xF8 | a; 這個的做用是把a的值保存了下來
* plain[0] = b; 而後把b作爲明文的第0個字節,這樣第0個字節就保存了a的信息,這個信息在解密時就要用來找到真正明文的起始位置
* plain[1 至 a+2] = 隨機數 & 0xFF; 這裏用隨機數填充明文的第1到第a+2個字節
* plain[a+3 至 a+3+明文長度-1] = 明文; 從a+3字節開始纔是真正的明文
* plain[a+3+明文長度, 最後] = 0; 在最後,填充0,填充到總長度爲8的整數爲止。到此爲止,結束了,這就是最後獲得的要加密的明文內容
* ------- 消息填充算法 ------------
*
* </code>
* </pre>
*
* @author luma
* @author notXX
*/
public class TeaUtil {
// 指向當前的明文塊
private byte[] plain;
// 這指向前面一個明文塊
private byte[] prePlain;
// 輸出的密文或者明文
private byte[] out;
// 當前加密的密文位置和上一次加密的密文塊位置,他們相差8
private int crypt, preCrypt;
// 當前處理的加密解密塊的位置
private int pos;
// 填充數
private int padding;
// 密鑰
private byte[] key;
// 用於加密時,表示當前是不是第一個8字節塊,由於加密算法是反饋的
// 可是最開始的8個字節沒有反饋可用,全部須要標明這種狀況
private boolean header = true;
// 這個表示當前解密開始的位置,之因此要這麼一個變量是爲了不當解密到最後時
// 後面已經沒有數據,這時候就會出錯,這個變量就是用來判斷這種狀況省得出錯
private int contextStart;
// 隨機數對象
private static Random random = new Random();
// 字節輸出流
private ByteArrayOutputStream baos;
/**
* 構造函數
*/
public TeaUtil() {
baos = new ByteArrayOutputStream(8);
}
/**
* 把字節數組從offset開始的len個字節轉換成一個unsigned int, 由於java裏面沒有unsigned,因此unsigned
* int使用long表示的, 若是len大於8,則認爲len等於8。若是len小於8,則高位填0 <br>
* (edited by notxx) 改變了算法, 性能稍微好一點. 在個人機器上測試10000次, 原始算法花費18s, 這個算法花費12s.
*
* @param in
* 字節數組.
* @param offset
* 從哪裏開始轉換.
* @param len
* 轉換長度, 若是len超過8則忽略後面的
* @return
*/
private static long getUnsignedInt(byte[] in, int offset, int len) {
long ret = 0;
int end = 0;
if (len > 8)
end = offset + 8;
else
end = offset + len;
for (int i = offset; i < end; i++) {
ret <<= 8;
ret |= in[i] & 0xff;
}
return (ret & 0xffffffffl) | (ret >>> 32);
}
/**
* 解密
* @param in 密文
* @param offset 密文開始的位置
* @param len 密文長度
* @param k 密鑰
* @return 明文
*/
public byte[] decrypt(byte[] in, int offset, int len, byte[] k) {
// 檢查密鑰
if(k == null)
return null;
crypt = preCrypt = 0;
this.key = k;
int count;
byte[] m = new byte[offset + 8];
// 由於QQ消息加密以後至少是16字節,而且確定是8的倍數,這裏檢查這種狀況
if((len % 8 != 0) || (len < 16)) return null;
// 獲得消息的頭部,關鍵是獲得真正明文開始的位置,這個信息存在第一個字節裏面,因此其用解密獲得的第一個字節與7作與
prePlain = decipher(in, offset);
pos = prePlain[0] & 0x7;
// 獲得真正明文的長度
count = len - pos - 10;
// 若是明文長度小於0,那確定是出錯了,好比傳輸錯誤之類的,返回
if(count < 0) return null;
// 這個是臨時的preCrypt,和加密時第一個8字節塊沒有prePlain同樣,解密時
// 第一個8字節塊也沒有preCrypt,全部這裏建一個全0的
for(int i = offset; i < m.length; i++)
m[i] = 0;
// 經過了上面的代碼,密文應該是沒有問題了,咱們分配輸出緩衝區
out = new byte[count];
// 設置preCrypt的位置等於0,注意目前的preCrypt位置是指向m的,由於java沒有指針,因此咱們在後面要控制當前密文buf的引用
preCrypt = 0;
// 當前的密文位置,爲何是8不是0呢?注意前面咱們已經解密了頭部信息了,如今固然該8了
crypt = 8;
// 天然這個也是8
contextStart = 8;
// 加1,和加密算法是對應的
pos++;
// 開始跳過頭部,若是在這個過程當中滿了8字節,則解密下一塊
// 由於是解密下一塊,因此咱們有一個語句 m = in,下一塊固然有preCrypt了,咱們再也不用m了
// 可是若是不滿8,這說明了什麼?說明了頭8個字節的密文是包含了明文信息的,固然仍是要用m把明文弄出來
// 因此,很顯然,滿了8的話,說明了頭8個字節的密文除了一個長度信息有用以外,其餘都是無用的填充
padding = 1;
while(padding <= 2) {
if(pos < 8) {
pos++;
padding++;
}
if(pos == 8) {
m = in;
if(!decrypt8Bytes(in, offset, len)) return null;
}
}
// 這裏是解密的重要階段,這個時候頭部的填充都已經跳過了,開始解密
// 注意若是上面一個while沒有滿8,這裏第一個if裏面用的就是原始的m,不然這個m就是in了
int i = 0;
while(count != 0) {
if(pos < 8) {
out[i] = (byte)(m[offset + preCrypt + pos] ^ prePlain[pos]);
i++;
count--;
pos++;
}
if(pos == 8) {
m = in;
preCrypt = crypt - 8;
if(!decrypt8Bytes(in, offset, len))
return null;
}
}
// 最後的解密部分,上面一個while已經把明文都解出來了,就剩下尾部的填充了,應該全是0
// 因此這裏有檢查是否解密了以後是否是0,若是不是的話那確定出錯了,返回null
for(padding = 1; padding < 8; padding++) {
if(pos < 8) {
if((m[offset + preCrypt + pos] ^ prePlain[pos]) != 0)
return null;
pos++;
}
if(pos == 8) {
m = in;
preCrypt = crypt;
if(!decrypt8Bytes(in, offset, len))
return null;
}
}
return out;
}
/**
* @param in
* 須要被解密的密文
* @paraminLen
* 密文長度
* @param k
* 密鑰
* @return Message 已解密的消息
*/
public byte[] decrypt(byte[] in, byte[] k) {
return decrypt(in, 0, in.length, k);
}
/**
* 加密
* @param in 明文字節數組
* @param offset 開始加密的偏移
* @param len 加密長度
* @param k 密鑰
* @return 密文字節數組
*/
public byte[] encrypt(byte[] in, int offset, int len, byte[] k) {
// 檢查密鑰
if(k == null)
return in;
plain = new byte[8];
prePlain = new byte[8];
pos = 1;
padding = 0;
crypt = preCrypt = 0;
this.key = k;
header = true;
// 計算頭部填充字節數
pos = (len + 0x0A) % 8;
if(pos != 0)
pos = 8 - pos;
// 計算輸出的密文長度
out = new byte[len + pos + 10];
// 這裏的操做把pos存到了plain的第一個字節裏面
// 0xF8後面三位是空的,正好留給pos,由於pos是0到7的值,表示文本開始的字節位置
plain[0] = (byte)((rand() & 0xF8) | pos);
// 這裏用隨機產生的數填充plain[1]到plain[pos]之間的內容
for(int i = 1; i <= pos; i++)
plain[i] = (byte)(rand() & 0xFF);
pos++;
// 這個就是prePlain,第一個8字節塊固然沒有prePlain,因此咱們作一個全0的給第一個8字節塊
for(int i = 0; i < 8; i++)
prePlain[i] = 0x0;
// 繼續填充2個字節的隨機數,這個過程當中若是滿了8字節就加密之
padding = 1;
while(padding <= 2) {
if(pos < 8) {
plain[pos++] = (byte)(rand() & 0xFF);
padding++;
}
if(pos == 8)
encrypt8Bytes();
}
// 頭部填充完了,這裏開始填真正的明文了,也是滿了8字節就加密,一直到明文讀完
int i = offset;
while(len > 0) {
if(pos < 8) {
plain[pos++] = in[i++];
len--;
}
if(pos == 8)
encrypt8Bytes();
}
// 最後填上0,以保證是8字節的倍數
padding = 1;
while(padding <= 7) {
if(pos < 8) {
plain[pos++] = 0x0;
padding++;
}
if(pos == 8)
encrypt8Bytes();
}
return out;
}
/**
* @param in
* 須要加密的明文
* @paraminLen
* 明文長度
* @param k
* 密鑰
* @return Message 密文
*/
public byte[] encrypt(byte[] in, byte[] k) {
return encrypt(in, 0, in.length, k);
}
/**
* 加密一個8字節塊
*
* @param in
* 明文字節數組
* @return
* 密文字節數組
*/
private byte[] encipher(byte[] in) {
// 迭代次數,16次
int loop = 0x10;
// 獲得明文和密鑰的各個部分,注意java沒有無符號類型,因此爲了表示一個無符號的整數
// 咱們用了long,這個long的前32位是全0的,咱們經過這種方式模擬無符號整數,後面用到的long也都是同樣的
// 並且爲了保證前32位爲0,須要和0xFFFFFFFF作一下位與
long y = getUnsignedInt(in, 0, 4);
long z = getUnsignedInt(in, 4, 4);
long a = getUnsignedInt(key, 0, 4);
long b = getUnsignedInt(key, 4, 4);
long c = getUnsignedInt(key, 8, 4);
long d = getUnsignedInt(key, 12, 4);
// 這是算法的一些控制變量,爲何delta是0x9E3779B9呢?
// 這個數是TEA算法的delta,實際是就是(sqr(5) - 1) * 2^31 (根號5,減1,再乘2的31次方)
long sum = 0;
long delta = 0x9E3779B9;
delta &= 0xFFFFFFFFL;
// 開始迭代了,亂七八糟的,我也看不懂,反正和DES之類的差很少,都是這樣倒來倒去
while (loop-- > 0) {
sum += delta;
sum &= 0xFFFFFFFFL;
y += ((z << 4) + a) ^ (z + sum) ^ ((z >>> 5) + b);
y &= 0xFFFFFFFFL;
z += ((y << 4) + c) ^ (y + sum) ^ ((y >>> 5) + d);
z &= 0xFFFFFFFFL;
}
// 最後,咱們輸出密文,由於我用的long,因此須要強制轉換一下變成int
baos.reset();
writeInt((int)y);
writeInt((int)z);
return baos.toByteArray();
}
/**
* 解密從offset開始的8字節密文
*
* @param in
* 密文字節數組
* @param offset
* 密文開始位置
* @return
* 明文
*/
private byte[] decipher(byte[] in, int offset) {
// 迭代次數,16次
int loop = 0x10;
// 獲得密文和密鑰的各個部分,注意java沒有無符號類型,因此爲了表示一個無符號的整數
// 咱們用了long,這個long的前32位是全0的,咱們經過這種方式模擬無符號整數,後面用到的long也都是同樣的
// 並且爲了保證前32位爲0,須要和0xFFFFFFFF作一下位與
long y = getUnsignedInt(in, offset, 4);
long z = getUnsignedInt(in, offset + 4, 4);
long a = getUnsignedInt(key, 0, 4);
long b = getUnsignedInt(key, 4, 4);
long c = getUnsignedInt(key, 8, 4);
long d = getUnsignedInt(key, 12, 4);
// 算法的一些控制變量,sum在這裏也有數了,這個sum和迭代次數有關係
// 由於delta是這麼多,因此sum若是是這麼多的話,迭代的時候減減減,減16次,最後
// 獲得0。反正這就是爲了獲得和加密時相反順序的控制變量,這樣才能解密呀~~
long sum = 0xE3779B90;
sum &= 0xFFFFFFFFL;
long delta = 0x9E3779B9;
delta &= 0xFFFFFFFFL;
// 迭代開始了, @_@
while(loop-- > 0) {
z -= ((y << 4) + c) ^ (y + sum) ^ ((y >>> 5) + d);
z &= 0xFFFFFFFFL;
y -= ((z << 4) + a) ^ (z + sum) ^ ((z >>> 5) + b);
y &= 0xFFFFFFFFL;
sum -= delta;
sum &= 0xFFFFFFFFL;
}
baos.reset();
writeInt((int)y);
writeInt((int)z);
return baos.toByteArray();
}
/**
* 寫入一個整型到輸出流,高字節優先
*
* @param t
*/
private void writeInt(int t) {
baos.write(t >>> 24);
baos.write(t >>> 16);
baos.write(t >>> 8);
baos.write(t);
}
/**
* 解密
*
* @param in
* 密文
* @return
* 明文
*/
private byte[] decipher(byte[] in) {
return decipher(in, 0);
}
/**
* 加密8字節
*/
private void encrypt8Bytes() {
// 這部分完成我上面所說的 plain ^ preCrypt,注意這裏判斷了是否是第一個8字節塊,若是是的話,那個prePlain就看成preCrypt用
for(pos = 0; pos < 8; pos++) {
if(header)
plain[pos] ^= prePlain[pos];
else
plain[pos] ^= out[preCrypt + pos];
}
// 這個完成我上面說的 f(plain ^ preCrypt)
byte[] crypted = encipher(plain);
// 這個沒什麼,就是拷貝一下,java不像c,因此我只好這麼幹,c就不用這一步了
System.arraycopy(crypted, 0, out, crypt, 8);
// 這個完成了 f(plain ^ preCrypt) ^ prePlain,ok,下面拷貝一下就好了
for(pos = 0; pos < 8; pos++)
out[crypt + pos] ^= prePlain[pos];
System.arraycopy(plain, 0, prePlain, 0, 8);
// 完成了加密,如今是調整crypt,preCrypt等等東西的時候了
preCrypt = crypt;
crypt += 8;
pos = 0;
header = false;
}
/**
* 解密8個字節
*
* @param in
* 密文字節數組
* @param offset
* 從何處開始解密
* @param len
* 密文的長度
* @return
* true表示解密成功
*/
private boolean decrypt8Bytes(byte[] in , int offset, int len) {
// 這裏第一步就是判斷後面還有沒有數據,沒有就返回,若是有,就執行 crypt ^ prePlain
for(pos = 0; pos < 8; pos++) {
if(contextStart + pos >= len)
return true;
prePlain[pos] ^= in[offset + crypt + pos];
}
// 好,這裏執行到了 d(crypt ^ prePlain)
prePlain = decipher(prePlain);
if(prePlain == null)
return false;
// 解密完成,最後一步好像沒作?
// 這裏最後一步放到decrypt裏面去作了,由於解密的步驟有點不太同樣
// 調整這些變量的值先
contextStart += 8;
crypt += 8;
pos = 0;
return true;
}
/**
* 這是個隨機因子產生器,用來填充頭部的,若是爲了調試,能夠用一個固定值
* 隨機因子可使相同的明文每次加密出來的密文都不同
*
* @return
* 隨機因子
*/
private int rand() {
return random.nextInt();
}
}
複製代碼
調用算法加解密: Test.java:算法
public class Test {
public static void main(String[] args) throws IOException {
byte[] KEY = new byte[]{//KEY
0x00000000, 0x00000000,
0x00000000, 0x00000000,
0x00000000, 0x00000000,
0x00000000, 0x00000000,
0x00000000, 0x00000000,
0x00000000, 0x00000000,
0x00000000, 0x00000000,
0x00000000, 0x00000000,
};
byte[] content = new byte[]{//加密內容
0x00000000, 0x00000000,
0x00000000, 0x00000000,
};
TeaUtil teaUtil = new TeaUtil();
byte[] enByte = teaUtil.encrypt(content,KEY); //加密後的字節
byte[] deByte = teaUtil.decrypt(enByte,KEY); //解密後的字節
}
}
複製代碼