QQ TEA加密算法 JAVA實現

image

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 = 隨機數 &amp; 0xF8 | a;              這個的做用是把a的值保存了下來
 *      plain[0] = b;                   而後把b作爲明文的第0個字節,這樣第0個字節就保存了a的信息,這個信息在解密時就要用來找到真正明文的起始位置
 *      plain[1 至 a+2] = 隨機數 &amp; 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); //解密後的字節
       
    }
}
複製代碼
相關文章
相關標籤/搜索