以上學習所有內容,對稱加密、非對稱加密、訊息摘要、數字簽名等知識都是為了理解數字證照工作原理而作為一個預備知識。數字證照是密碼學裡的終極武器,是人類幾千年歷史總結的智慧的結晶,只有在明白了數字證照工作原理後,才能理解Https 協議的安全通訊機制。最終才能在SSL 開發過程中得心應手。
另外,對稱加密和訊息摘要這兩個知識點是可以單獨拿來使用的。
數字證照使用到了以上學習的所有知識
- 對稱加密與非對稱加密結合使用實現了祕鑰交換,之後通訊雙方使用該祕鑰進行對稱加密通訊。
- 訊息摘要與非對稱加密實現了數字簽名,根證照機構對目標證照進行簽名,在校驗的時候,根證照用公鑰對其進行校驗。若校驗成功,則說明該證照是受信任的。
- Keytool 工具可以建立證照,之後交給根證照機構認證後直接使用自簽名證照,還可以輸出證照的RFC格式資訊等。
- 數字簽名技術實現了身份認證與資料完整性保證。
- 加密技術保證了資料的保密性,訊息摘要演算法保證了資料的完整性,對稱加密的高效保證了資料處理的可靠性,數字簽名技術保證了操作的不可否認性。
通過以上內容的學習,我們要能掌握以下知識點:
- 基礎知識:bit 位、位元組、字元、字元編碼、進位制轉換、io
- 知道怎樣在實際開發裡怎樣使用對稱加密解決問題
- 知道對稱加密、非對稱加密、訊息摘要、數字簽名、數字證照是為了解決什麼問題而出現的
- 瞭解SSL 通訊流程
- 實際開發裡怎樣請求Https 的介面
凱撒密碼
1. 介紹
凱撒密碼作為一種最為古老的對稱加密體制,在古羅馬的時候都已經很流行,他的基本思想是:通過把字母移動一定的位數來實現加密和解密。明文中的所有字母都在字母表上向後(或向前)按照一個固定數目進行偏移後被替換成密文。例如,當偏移量是3 的時候,所有的字母A 將被替換成D,B 變成E,由此可見,位數就是凱撒密碼加密和解密的金鑰。
例如:字串”ABC”的每個字元都右移3 位則變成”DEF”,解密的時候”DEF”的每個字元左移3 位即能還原,如下圖所示:
2. 準備知識
1 2 3 4 5 6 7 |
//字元轉換成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. 凱撒密碼的簡單程式碼實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
/** * 加密 * @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’偏移後的值。
解密時要多幾次嘗試,因為不一定出現次數最多的字元就是我們想要的目標字元,如下圖,第二次解密的結果才是正確的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
/** * 頻率分析法破解凱撒密碼 */ 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(); } } } } } |
對稱加密
介紹
加密和解密都使用同一把祕鑰,這種加密方法稱為對稱加密,也稱為單金鑰加密。
簡單理解為:加密解密都是同一把鑰匙。
凱撒密碼就屬於對稱加密,他的字元偏移量即為祕鑰。
對稱加密常用演算法
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 年,高階加密標準已然成為對稱金鑰加密中最流行的演算法之一。
DES 演算法簡介
DES 加密原理(對位元位進行操作,交換位置,異或等等,無需詳細瞭解)
準備知識
Bit 是計算機最小的傳輸單位。以0 或1 來表示位元位的值
例如數字3 對應的二進位制資料為:00000011
程式碼示例
1 2 3 4 |
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 的取值範圍:
1 2 |
//byte 的取值範圍:-128 到127 System.out.println(Byte.MIN_VALUE + "到" + Byte.MAX_VALUE); |
即10000000 到01111111 之間,一個位元組佔8 個位元位
二進位制轉十進位制圖示:
任何字串都可以轉換為位元組陣列
1 2 |
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 置換:
S-BOX 置換:
DES 加密過程圖解(流程很複雜,只需要知道內部是操作位元位即可):
對稱加密應用場景
- 本地資料加密(例如加密android 裡SharedPreferences 裡面的某些敏感資料)
- 網路傳輸:登入介面post 請求引數加密{username=lisi,pwd=oJYa4i9VASRoxVLh75wPCg==}
- 加密使用者登入結果資訊並序列化到本地磁碟(將user 物件序列化到本地磁碟,下次登入時反序列化到記憶體裡)
- 網頁互動資料加密(即後面學到的Https)
DES 演算法程式碼實現
1 2 3 4 5 6 7 8 |
//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 編碼加密後的結果
1 2 |
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 字元對映表
對稱加密的具體應用方式
1. 生成祕鑰並儲存到硬碟上,以後讀取該祕鑰進行加密解密操作,實際開發中用得比較少
1 2 3 4 5 6 7 8 9 10 11 12 |
//生成隨機祕鑰 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 2 3 4 5 6 7 8 9 10 11 |
//建立金鑰寫法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 物件時,引數可以直接傳演算法名:例如:
1 |
Cipher c = Cipher.getInstance("DES"); |
也可以指定更詳細的引數,格式:”algorithm/mode/padding” ,即”演算法/工作模式/填充模式”
1 |
Cipher c = Cipher.getInstance("DES/CBC/PKCS5Padding"); |
密碼塊工作模式
塊密碼工作模式(Block cipher mode of operation),是對於按塊處理密碼的加密方式的一種擴充,不僅僅適用於AES,包括DES, RSA 等加密方法同樣適用。
填充模式
填充(Padding),是對需要按塊處理的資料,當資料長度不符合塊處理需求時,按照一定方法填充滿塊長的一種規則。
具體程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//祕鑰演算法 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 引數
1 2 3 4 5 |
//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 裡的時候,也可以使用對稱加密,這樣可以在需要的時候還原。
- 請求網路介面的時候,我們需要上傳一些敏感資料,同樣也可以使用對稱加密,服務端使用同樣的演算法就可以解密。或者服務端需要給客戶端傳遞資料,同樣也可以先加密,然後客戶端使用同樣演算法解密。