QQ TEA加密演算法 JAVA實現

薛8發表於2019-02-07
image

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 = 隨機數 &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); //解密後的位元組
       
    }
}
複製程式碼

相關文章