TeaUtil.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); //解密後的位元組
}
}
複製程式碼