Android安全加密:對稱加密

Android安全加密專題文章索引

  1. Android安全加密:對稱加密
  2. Android安全加密:非對稱加密
  3. Android安全加密:消息摘要Message Digest
  4. Android安全加密:數字簽名和數字證書
  5. Android安全加密:Https編程

一、凱撒密碼

1. 概述

凱撒密碼作爲一種最爲古老的對稱加密體制,在古羅馬的時候都已經很流行,他的基本思想是:通過把字母移動一定的位數來實現加密和解密。明文中的所有字母都在字母表上向後(或向前)按照一個固定數目進行偏移後被替換成密文。例如,當偏移量是3 的時候,所有的字母A 將被替換成D,B 變成E,由此可見,位數就是凱撒密碼加密和解密的密鑰。

例如:字符串”ABC”的每個字符都右移3 位則變成”DEF”,解密的時候”DEF”的每個字符左移3 位即能還原,如下圖所示:

這裏寫圖片描述

2. 準備知識

 //字符轉換成ASCII 碼數值
 char charA = 'a';
 int intA = charA; //char 強轉爲int 即得到對應的ASCII 碼值,’a’的值爲97

//ASCII 碼值轉成char
int intA = 97;//97 對應的ASCII 碼’a’
char charA = (char) intA; //int 值強轉爲char 即得到對應的ASCII 字符,即'a'

這裏寫圖片描述

3. 凱撒密碼的簡單代碼實現

    /**
     * 加密
     * @param input 數據源(需要加密的數據)
     * @param key 祕鑰,即偏移量
     * @return 返回加密後的數據
     */
    public static String encrypt(String input, int key) {
        //得到字符串裏的每一個字符
        char[] array = input.toCharArray();

        for (int i = 0; i < array.length; ++i) {
            //字符轉換成ASCII 碼值
            int ascii = array[i];
            //字符偏移,例如a->b
            ascii = ascii + key;
            //ASCII 碼值轉換爲char
            char newChar = (char) ascii;
            //替換原有字符
            array[i] = newChar;

            //以上4 行代碼可以簡寫爲一行
            //array[i] = (char) (array[i] + key);
        }

        //字符數組轉換成String
        return new String(array);
    }

    /**
     * 解密
     * @param input 數據源(被加密後的數據)
     * @param key 祕鑰,即偏移量
     * @return 返回解密後的數據
     */
    public static String decrypt(String input, int key) {
        //得到字符串裏的每一個字符
        char[] array = input.toCharArray();
        for (int i = 0; i < array.length; ++i) {
            //字符轉換成ASCII 碼值
            int ascii = array[i];
            //恢復字符偏移,例如b->a
            ascii = ascii - key;
            //ASCII 碼值轉換爲char
            char newChar = (char) ascii;
            //替換原有字符
            array[i] = newChar;

            //以上4 行代碼可以簡寫爲一行
            //array[i] = (char) (array[i] - key);
        }

        //字符數組轉換成String
        return new String(array);
    }

代碼輸出結果:
這裏寫圖片描述

4. 破解凱撒密碼:頻率分析法

凱撒密碼加密強度太低,只需要用頻度分析法即可破解。
在任何一種書面語言中,不同的字母或字母組合出現的頻率各不相同。而且,對於以這種語言書寫的任意一段文本,都具有大致相同的特徵字母分佈。比如,在英語中,字母E 出現的頻率很高,而X 則出現得較少。

英語文本中典型的字母分佈情況如下圖所示:
這裏寫圖片描述

5. 破解流程

  • 統計密文裏出現次數最多的字符,例如出現次數最多的字符是是’h’。
  • 計算字符’h’到’e’的偏移量,值爲3,則表示原文偏移了3 個位置。
  • 將密文所有字符恢復偏移3 個位置。

注意點:統計密文裏出現次數最多的字符時,需多統計幾個備選,因爲最多的可能是空格或者其他字符,例如下圖出現次數最多的字符’#’是空格加密後的字符,’h’纔是’e’偏移後的值。
這裏寫圖片描述

解密時要多幾次嘗試,因爲不一定出現次數最多的字符就是我們想要的目標字符,如下圖,第二次解密的結果纔是正確的。

/**
 * 頻率分析法破解凱撒密碼
 */
public class FrequencyAnalysis {
    //英文裏出現次數最多的字符
    private static final char MAGIC_CHAR = 'e';
    //破解生成的最大文件數
    private static final int DE_MAX_FILE = 4;

    public static void main(String[] args) throws Exception {
        //測試1,統計字符個數
        //printCharCount("article1_en.txt");

        //加密文件
        //int key = 3;
        //encryptFile("article1.txt", "article1_en.txt", key);

        //讀取加密後的文件
        String artile = file2String("article1_en.txt");
        //解密(會生成多個備選文件)
        decryptCaesarCode(artile, "article1_de.txt");
    }

    public static void printCharCount(String path) throws IOException{
        String data = file2String(path);
        List<Entry<Character, Integer>> mapList = getMaxCountChar(data);
        for (Entry<Character, Integer> entry : mapList) {
            //輸出前幾位的統計信息
            System.out.println("字符'" + entry.getKey() + "'出現" + entry.getValue() + "次");
        }
    }

    public static void encryptFile(String srcFile, String destFile, int key) throws IOException {
        String artile = file2String(srcFile);
        //加密文件
        String encryptData = MyEncrypt.encrypt(artile, key);
        //保存加密後的文件
        string2File(encryptData, destFile);
    }

    /**
     * 破解凱撒密碼
     * @param input 數據源
     * @return 返回解密後的數據
     */
    public static void decryptCaesarCode(String input, String destPath) {
        int deCount = 0;//當前解密生成的備選文件數
        //獲取出現頻率最高的字符信息(出現次數越多越靠前)
        List<Entry<Character, Integer>> mapList = getMaxCountChar(input);
        for (Entry<Character, Integer> entry : mapList) {
            //限制解密文件備選數
            if (deCount >= DE_MAX_FILE) {
                break;
            }

            //輸出前幾位的統計信息
            System.out.println("字符'" + entry.getKey() + "'出現" + entry.getValue() + "次");

            ++deCount;
            //出現次數最高的字符跟MAGIC_CHAR的偏移量即爲祕鑰
            int key = entry.getKey() - MAGIC_CHAR;
            System.out.println("猜測key = " + key + ", 解密生成第" + deCount + "個備選文件" + "\n");
            String decrypt = MyEncrypt.decrypt(input, key);

            String fileName = "de_" + deCount + destPath;
            string2File(decrypt, fileName);
        }
    }

    //統計String裏出現最多的字符
    public static List<Entry<Character, Integer>> getMaxCountChar(String data) {
        Map<Character, Integer> map = new HashMap<Character, Integer>();
        char[] array = data.toCharArray();
        for (char c : array) {
            if(!map.containsKey(c)) {
                map.put(c, 1);
            }else{
                Integer count = map.get(c);
                map.put(c, count + 1);
            }
        }

        //輸出統計信息
        /*for (Entry<Character, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + "出現" + entry.getValue() +  "次");
        }*/

        //獲取獲取最大值
        int maxCount = 0;
        for (Entry<Character, Integer> entry : map.entrySet()) {
            //不統計空格
            if (/*entry.getKey() != ' ' && */entry.getValue() > maxCount) { 
                maxCount = entry.getValue();
            }
        }

        //map轉換成list便於排序
        List<Entry<Character, Integer>> mapList = new ArrayList<Map.Entry<Character,Integer>>(map.entrySet());
        //根據字符出現次數排序
        Collections.sort(mapList, new Comparator<Entry<Character, Integer>>(){
            @Override
            public int compare(Entry<Character, Integer> o1,
                    Entry<Character, Integer> o2) {
                return o2.getValue().compareTo(o1.getValue());
            }
        });
        return mapList;
    }

    public static String file2String(String path) throws IOException {
        FileReader reader = new FileReader(new File(path));
        char[] buffer = new char[1024];
        int len = -1;
        StringBuffer sb = new StringBuffer();
        while ((len = reader.read(buffer)) != -1) {
            sb.append(buffer, 0, len);
        }
        return sb.toString();
    }

    public static void string2File(String data, String path){
        FileWriter writer = null;
        try {
            writer = new FileWriter(new File(path));
            writer.write(data);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

這裏寫圖片描述

二、對稱加密

1、概述

加密和解密都使用同一把祕鑰,這種加密方法稱爲對稱加密,也稱爲單密鑰加密。
簡單理解爲:加密解密都是同一把鑰匙。
凱撒密碼就屬於對稱加密,他的字符偏移量即爲祕鑰。

2、對稱加密常用算法

AES、DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK 等。

DES
全稱爲Data Encryption Standard,即數據加密標準,是一種使用密鑰加密的塊算法,1976 年被美國聯邦政府的國家標準局確定爲聯邦資料處理標準(FIPS),隨後在國際上廣泛流傳開來。

3DES
也叫Triple DES,是三重數據加密算法(TDEA,Triple Data Encryption Algorithm)塊密碼的通稱。
它相當於是對每個數據塊應用三次DES 加密算法。由於計算機運算能力的增強,原版DES 密碼的密鑰長度變得容易被暴力破解;3DES 即是設計用來提供一種相對簡單的方法,即通過增加DES 的密鑰長度來避免類似的攻擊,而不是設計一種全新的塊密碼算法。

AES
高級加密標準(英語:Advanced Encryption Standard,縮寫:AES),在密碼學中又稱Rijndael 加密法,是美國聯邦政府採用的一種區塊加密標準。這個標準用來替代原先的DES,已經被多方分析且廣爲全世界所使用。經過五年的甄選流程,高級加密標準由美國國家標準與技術研究院(NIST)於2001 年11 月26 日發佈於FIPS PUB 197,並在2002 年5 月26 日成爲有效的標準。2006 年,高級加密標準已然成爲對稱密鑰加密中最流行的算法之一。

3、DES 算法簡介

DES 加密原理(對比特位進行操作,交換位置,異或等等,無需詳細瞭解)

準備知識

Bit 是計算機最小的傳輸單位。以0 或1 來表示比特位的值
例如數字3 對應的二進制數據爲:00000011

代碼示例

 int i = 97;
 String bit = Integer.toBinaryString(i);
 //輸出:97 對應的二進制數據爲: 1100001
 System.out.println(i + "對應的二進制數據爲: " + bit);

Byte 與Bit 區別

數據存儲是以“字節”(Byte)爲單位,數據傳輸是大多是以“位”(bit,又名“比特”)爲單位,一個位就代表一個0 或1(即二進制),每8 個位(bit,簡寫爲b)組成一個字節(Byte,簡寫爲B),是最小一級的信息單位。

Byte 的取值範圍:

//byte 的取值範圍:-128 到127
System.out.println(Byte.MIN_VALUE + "到" + Byte.MAX_VALUE);

即10000000 到01111111 之間,一個字節佔8 個比特位

二進制轉十進制圖示:
這裏寫圖片描述

任何字符串都可以轉換爲字節數組

String data = "1234abcd";
byte[] bytes = data.getBytes();//內容爲:49 50 51 52 97 98 99 100

上面數據49 50 51 52 97 98 99 100 對應的二進制數據(即比特位爲):
00110001
00110010
00110011
00110100
01100001
01100010
01100011
01100100

將他們間距調大一點,可看做一個矩陣:
這裏寫圖片描述

之後可對他們進行各種操作,例如交換位置、分割、異或運算等,常見的加密方式就是這樣操作比特位的,例如下圖的IP 置換以及S-Box 操作都是常見加密的一些方式:

IP 置換:
IP 置換:

S-BOX 置換:
這裏寫圖片描述

DES 加密過程圖解(流程很複雜,只需要知道內部是操作比特位即可):

這裏寫圖片描述

對稱加密應用場景

  • 本地數據加密(例如加密android 裏SharedPreferences 裏面的某些敏感數據)
  • 網絡傳輸:登錄接口post 請求參數加密{username=lisi,pwd=oJYa4i9VASRoxVLh75wPCg==}
  • 加密用戶登錄結果信息並序列化到本地磁盤(將user 對象序列化到本地磁盤,下次登錄時反序列化到內存裏)
  • 網頁交互數據加密(即後面學到的Https)

DES 算法代碼實現

 //1,得到cipher 對象(可翻譯爲密碼器或密碼系統)
 Cipher cipher = Cipher.getInstance("DES");
 //2,創建祕鑰
 SecretKey key = KeyGenerator.getInstance("DES").generateKey();
 //3,設置操作模式(加密/解密)
 cipher.init(Cipher.ENCRYPT_MODE, key);
 //4,執行操作
 byte[] result = cipher.doFinal("黑馬".getBytes());

AES 算法代碼實現

用法同上,只需把”DES”參數換成”AES”即可。

使用Base64 編碼加密後的結果

byte[] result = cipher.doFinal("黑馬".getBytes());
System.out.println(new String(result));

輸出結果:

這裏寫圖片描述

加密後的結果是字節數組,這些被加密後的字節在碼錶(例如UTF-8 碼錶)上找不到對應字符,會出現亂碼,當亂碼字符串再次轉換爲字節數組時,長度會變化,導致解密失敗,所以轉換後的數據是不安全的。

使用Base64 對字節數組進行編碼,任何字節都能映射成對應的Base64 字符,之後能恢復到字節數組,利於加密後數據的保存於傳輸,所以轉換是安全的。同樣,字節數組轉換成16 進制字符串也是安全的。

密文轉換成Base64 編碼後的輸出結果:
這裏寫圖片描述

密文轉換成16 進制編碼後的輸出結果:
這裏寫圖片描述

Java 裏沒有直接提供Base64 以及字節數組轉16 進制的Api,開發中一般是自己手寫或直接使用第三方提供的成熟穩定的工具類(例如apache 的commons-codec)。

Base64 字符映射表
Base64 字符映射表

對稱加密的具體應用方式

1、生成祕鑰並保存到硬盤上,以後讀取該祕鑰進行加密解密操作,實際開發中用得比較少

//生成隨機祕鑰
SecretKey secretKey = KeyGenerator.getInstance("AES").generateKey();
//序列化祕鑰到磁盤上
FileOutputStream fos = new FileOutputStream(new File("heima.key"));

ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(secretKey);

//從磁盤裏讀取祕鑰
FileInputStream fis = new FileInputStream(new File("heima.key"));
ObjectInputStream ois = new ObjectInputStream(fis);
Key key = (Key) ois.readObject();

2、使用自定義祕鑰(祕鑰寫在代碼裏)

//創建密鑰寫法1
KeySpec keySpec = new DESKeySpec(key.getBytes());
SecretKey secretKey = SecretKeyFactory.getInstance(ALGORITHM).
generateSecret(keySpec);

//創建密鑰寫法2
//SecretKey secretKey = new SecretKeySpec(key.getBytes(), KEY_ALGORITHM);

Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
//得到key 後,後續代碼就是Cipher 的寫法,此處省略...

注意事項

把祕鑰寫在代碼裏有一定風險,當別人反編譯代碼的時候,可能會看到祕鑰,android 開發裏建議用JNI 把祕鑰值寫到C 代碼裏,甚至拆分成幾份,最後再組合成真正的祕鑰

算法/工作模式/填充模式

初始化cipher 對象時,參數可以直接傳算法名:例如:

Cipher c = Cipher.getInstance("DES");

也可以指定更詳細的參數,格式:”algorithm/mode/padding” ,即”算法/工作模式/填充模式”

Cipher c = Cipher.getInstance("DES/CBC/PKCS5Padding");

密碼塊工作模式

塊密碼工作模式(Block cipher mode of operation),是對於按塊處理密碼的加密方式的一種擴充,不僅僅適用於AES,包括DES, RSA 等加密方法同樣適用。

這裏寫圖片描述

填充模式

填充(Padding),是對需要按塊處理的數據,當數據長度不符合塊處理需求時,按照一定方法填充滿塊長的一種規則。

這裏寫圖片描述

具體代碼:

//祕鑰算法
private static final String KEY_ALGORITHM = "DES";
//加密算法:algorithm/mode/padding 算法/工作模式/填充模式
private static final String CIPHER_ALGORITHM = "DES/ECB/PKCS5Padding";
//祕鑰
private static final String KEY = "12345678";//DES 祕鑰長度必須是8 位或以上
//private static final String KEY = "1234567890123456";//AES 祕鑰長度必須是16 位

//初始化祕鑰
SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), KEY_ALGORITHM);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

//加密
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] result = cipher.doFinal(input.getBytes());

注意:AES、DES 在CBC 操作模式下需要iv 參數

//AES、DES 在CBC 操作模式下需要iv 參數
IvParameterSpec iv = new IvParameterSpec(key.getBytes());

//加密
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);

三、總結

DES 安全度在現代已經不夠高,後來又出現的3DES 算法強度提高了很多,但是其執行效率低下,AES算法加密強度大,執行效率高,使用簡單,實際開發中建議選擇AES 算法。實際android 開發中可以用對稱加密(例如選擇AES 算法)來解決很多問題,例如:

  • 做一個管理密碼的app,我們在不同的網站裏使用不同賬號密碼,很難記住,想做個app 統一管理,但是賬號密碼保存在手機裏,一旦丟失了容易造成安全隱患,所以需要一種加密算法,將賬號密碼信息加密起來保管,這時候如果使用對稱加密算法,將數據進行加密,祕鑰我們自己記在心裏,只需要記住一個密碼。需要的時候可以還原信息。
  • android 裏需要把一些敏感數據保存到SharedPrefrence 裏的時候,也可以使用對稱加密,這樣可以在需要的時候還原。
  • 請求網絡接口的時候,我們需要上傳一些敏感數據,同樣也可以使用對稱加密,服務端使用同樣的算法就可以解密。或者服務端需要給客戶端傳遞數據,同樣也可以先加密,然後客戶端使用同樣算法解密。

Android視頻教程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章