第一章:密碼學發展歷史與基礎知識
一、密碼學基本概念
密碼在我們的生活中有著重要的作用,那麼密碼究竟來自何方,為何會產生呢?
密碼學是網路安全、資訊保安、區塊鏈等產品的基礎,常見的非對稱加密、對稱加密、雜湊函式等,都屬於密碼學範疇。
密碼學有數千年的歷史,從最開始的替換法到如今的非對稱加密演算法,經歷了古典密碼學,近代密碼學和現代密碼學三個階段。密碼學不僅僅是數學家們的智慧,更是如今網路空間安全的重要基礎。
二、密碼學的發展歷史
1、古典密碼學
在古代的戰爭中,多見使用隱藏資訊的方式保護重要的通訊資料。比如先把需要保護的資訊用化學藥水寫到紙上,藥水幹後,紙上看不出任何的資訊,需要使用另外的化學藥水塗抹後才可以閱讀紙上的資訊。
例:https://www.iqiyi.com/v_19rt6ab1hg.html
這些方法都是在保護重要的資訊不被他人獲取,但藏資訊的方式比較容易被他人識破,例如增加哨兵的排查力度,就會發現其中的貓膩,因而隨後發展出了較難破解的古典密碼學。
【1】替換法
替換法很好理解,就是用固定的資訊將原文替換成無法直接閱讀的密文資訊。例如將 b 替換成 w ,e 替換成 p ,這樣 bee 單詞就變換成了 wpp,不知道替換規則的人就無法閱讀出原文的含義。
替換法有單表替換和多表替換兩種形式。
單表替換即只有一張原文密文對照表單,傳送者和接收者用這張表單來加密解密。
在上述例子中,表單即為:a b c d e - s w t r p 。
多表替換即有多張原文密文對照表單,不同字母可以用不同表單的內容替換。
例如約定好表單為:表單 1:abcde-swtrp 、表單2:abcde-chfhk 、表單 3:abcde-jftou。
規定第一個字母用第三張表單,第二個字母用第一張表單,第三個字母用第二張表單,這時 bee單詞就變成了
(312)fpk ,破解難度更高,其中 312 又叫做金鑰,金鑰可以事先約定好,也可以在傳輸過程中標記出來。
【2】移位法
移位法就是將原文中的所有字母都在字母表上向後(或向前)按照一個固定數目進行偏移後得出密文,典型的移位法應用有 "愷撒密碼"。
例如約定好向後移動2位(abcde - cdefg),這樣 bee 單詞就變換成了dgg 。
同理替換法,移位法也可以採用多表移位的方式,典型的多表案例是“維尼吉亞密碼”(又譯維熱納爾密碼),屬於多表密碼的一種形式。
【3】古典密碼破解方式
古典密碼雖然很簡單,但是在密碼史上是使用的最久的加密方式,直到“概率論”的數學方法被發現,古典密碼就被破解了。
英文單詞中字母出現的頻率是不同的,e以12.702%的百分比佔比最高,z 只佔到0.074%,感興趣的可以去百科查字母頻率詳細統計資料。如果密文數量足夠大,僅僅採用頻度分析法就可以破解單表的替換法或移位法。
多表的替換法或移位法雖然難度高一些,但如果資料量足夠大的話,也是可以破解的。以維尼吉亞密碼演算法為例,破解方法就是先找出密文中完全相同的字母串,猜測金鑰長度,得到金鑰長度後再把同組的密文放在一起,使用頻率分析法破解。
2、近代密碼學
古典密碼的安全性受到了威脅,外加使用便利性較低,到了工業化時代,近現代密碼被廣泛應用。
恩尼格瑪機
恩尼格瑪機是二戰時期納粹德國使用的加密機器,後被英國破譯,參與破譯的人員有被稱為電腦科學之父、人工智慧之父的圖靈。
恩尼格瑪機
恩尼格瑪機使用的加密方式本質上還是移位和替代,只不過因為密碼錶種類極多,破解難度高,同時加密解密機器化,使用便捷,因而在二戰時期得以使用。
3、現代密碼學
【1】雜湊函式
雜湊函式,也見雜湊函式、摘要函式或雜湊函式,可將任意長度的訊息經過運算,變成固定長度數值,常見的有MD5、SHA-1、SHA256,多應用在檔案校驗,數字簽名中。
MD5 可以將任意長度的原文生成一個128位(16位元組)的雜湊值
SHA-1可以將任意長度的原文生成一個160位(20位元組)的雜湊值
【2】對稱密碼
對稱密碼應用了相同的加密金鑰和解密金鑰。對稱密碼分為:序列密碼(流密碼),分組密碼(塊密碼)兩種。流密碼是對資訊流中的每一個元素(一個字母或一個位元)作為基本的處理單元進行加密,塊密碼是先對資訊流分塊,再對每一塊分別加密。
例如原文為1234567890,流加密即先對1進行加密,再對2進行加密,再對3進行加密……最後拼接成密文;塊加密先分成不同的塊,如1234成塊,5678成塊,90XX(XX為補位數字)成塊,再分別對不同塊進行加密,最後拼接成密文。前文提到的古典密碼學加密方法,都屬於流加密。
【3】非對稱密碼
對稱密碼的金鑰安全極其重要,加密者和解密者需要提前協商金鑰,並各自確保金鑰的安全性,一但金鑰洩露,即使演算法是安全的也無法保障原文資訊的私密性。
在實際的使用中,遠端的提前協商金鑰不容易實現,即使協商好,在遠端傳輸過程中也容易被他人獲取,因此非對稱金鑰此時就凸顯出了優勢。
非對稱密碼有兩支金鑰,公鑰(publickey)和私鑰(privatekey),加密和解密運算使用的金鑰不同。用公鑰對原文進行加密後,需要由私鑰進行解密;用私鑰對原文進行加密後(此時一般稱為簽名),需要由公鑰進行解密(此時一般稱為驗籤)。公鑰可以公開的,大家使用公鑰對資訊進行加密,再發送給私鑰的持有者,私鑰持有者使用私鑰對資訊進行解密,獲得資訊原文。因為私鑰只有單一人持有,因此不用擔心被他人解密獲取資訊原文。
三、如何設定密碼才安全
- 密碼不要太常見,不要使用類似於123456式的常用密碼。
- 各應用軟體密碼建議不同,避免出現一個應用資料庫被脫庫,全部應用密碼崩塌,
- 可在設定密碼時增加註冊時間、註冊地點、應用特性等方法。例如tianjin123456,表示在天津註冊的該應用;zfb123456(支付寶) 使用字首等方式
四、ASCII編碼
ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼)是基於拉丁字母的一套電腦編碼系統,主要用於顯示現代英語和其他西歐語言。它是現今最通用的單位元組編碼系統,並等同於國際標準ISO/IEC 646。
示例程式碼:
建立maven專案 encrypt-decrypt
(1)新增pom檔案
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
(2)建立類 com.njf.ascii.AsciiDemo,字元轉換成ascii碼
public class AsciiDemo {
public static void main(String[] args) {
char a = 'A';
int b = a;
// 列印ascii碼
System.out.println(b);
}
}
執行程式
(3)字串轉換成ascii碼
public class AsciiDemo {
public static void main(String[] args) {
char a = 'A';
int b = a;
// 列印ascii碼
System.out.println(b);
System.out.println("---列印字串的Ascii碼---");
String str = "AaZ";
// 獲取ascii碼,需要把字串轉成字元
char[] chars = str.toCharArray();
for (char c : chars) {
int asciiCode = c;
System.out.println(asciiCode);
}
}
}
列印字串的ascii碼值:
五、愷撒加密
1、中國古代加密
看一個小故事 , 看看古人如何加密和解密:
公元683年,唐中宗即位。隨後,武則天廢唐中宗,立第四子李旦為皇帝,但朝政大事均由她自己專斷。
裴炎、徐敬業和駱賓王等人對此非常不滿。徐敬業聚兵十萬,在江蘇揚州起兵。裴炎做內應,欲以拆字手段為其傳遞祕密資訊。後因有人告密,裴炎被捕,未發出的密信落到武則天手中。這封密信上只有“青鵝”二字,群臣對此大惑不解。
武則天破解了“青鵝”的祕密:“青”字拆開來就是“十二月”,而“鵝”字拆開來就是“我自與”。密信的意思是讓徐敬業、駱賓王等率兵於十二月進發,裴炎在內部接應。“青鵝”破譯後,裴炎被殺。接著,武則天派兵擊敗了徐敬業和駱賓王。
2、外國加密
在密碼學中,愷撒密碼是一種最簡單且最廣為人知的加密技術。
凱撒密碼最早由古羅馬軍事統帥蓋烏斯·尤利烏斯·凱撒在軍隊中用來傳遞加密資訊,故稱凱撒密碼。這是一種位移加密方式,只對26個字母進行位移替換加密,規則簡單,容易破解。下面是位移1次的對比:
將明文字母表向後移動1位,A變成了B,B變成了C……,Z變成了A。同理,若將明文字母表向後移動3位:
則A變成了D,B變成了E……,Z變成了C。
字母表最多可以移動25位。凱撒密碼的明文字母表向後或向前移動都是可以的,通常表述為向後移動,如果要向前移動1位,則等同於向後移動25位,位移選擇為25即可。
它是一種替換加密的技術,明文中的所有字母都在字母表上向後(或向前)按照一個固定數目進行偏移後被替換成密文。
例如,當偏移量是3的時候,所有的字母A將被替換成D,B變成E,以此類推。
這個加密方法是以愷撒的名字命名的,當年愷撒曾用此方法與其將軍們進行聯絡。
愷撒密碼通常被作為其他更復雜的加密方法中的一個步驟。
簡單來說就是當祕鑰為n,其中一個待加密字元ch,加密之後的字元為ch+n,當ch+n超過’z’時,回到’a’計數。
3、凱撒位移加密
建立類 KaiserDemo,把 hello world 往右邊移動3位
public class KaiserDemo {
public static void main(String[] args) {
String input = "Hello world";
// 往右邊移動3位(金鑰)
int key = 3;
// 用來拼接
StringBuilder sb = new StringBuilder();
// 字串轉換成位元組陣列
char[] chars = input.toCharArray();
for (char c : chars) {
int asciiCode = c;
// 使用金鑰計算加密後的結果
asciiCode += key;
char newChar = (char) asciiCode;
sb.append(newChar);
}
System.out.println(sb.toString());
}
}
執行結果:
4、凱撒加密和解密
public class KaiserDemo {
public static void main(String[] args) {
String input = "Hello world";
// 往右邊移動3位(金鑰)
int key = 3;
String encryptStr = encryptKaiser(input, key);
System.out.println("密文為:encryptStr = " + encryptStr);
String decryptStr = decryptKaiser(encryptStr, key);
System.out.println("解密後:decryptStr = " + decryptStr);
}
/**
* 用凱撒加密方式解密資料
* @param encryptData 密文
* @param key 金鑰
* @return 解密後的資料
*/
public static String decryptKaiser(String encryptData, int key) {
// 用來拼接
StringBuilder sb = new StringBuilder();
//轉換為位元組陣列
char[] chars = encryptData.toCharArray();
for (char c : chars) {
//使用金鑰進行解密
int asciiCode = c;
// 偏移資料
asciiCode -= key;
// 將偏移後的資料轉為字元
char newChar = (char) asciiCode;
// 拼接資料
sb.append(newChar);
}
return sb.toString();
}
/**
* 用凱撒加密方式加密資料
* @param origin 原文
* @param key 金鑰
* @return 加密後的資料
*/
public static String encryptKaiser(String origin, int key) {
// 用來拼接
StringBuilder sb = new StringBuilder();
//轉換為位元組陣列
char[] chars = origin.toCharArray();
//對位元組中的每個字元進行加密操作
for (char c : chars) {
int asciiCode = c;
// 使用金鑰計算加密後的結果
asciiCode += key;
char newChar = (char) asciiCode;
sb.append(newChar);
}
return sb.toString();
}
}
六、頻度分析法破解愷撒加密
1、密碼棒
公元前5世紀的時候,斯巴達人利用一根木棒,纏繞上皮革或者羊皮紙,在上面橫向寫下資訊,解下這條皮帶。展開來看,這長串字母沒有任何意義。
比如這樣:
信差可以將這條皮帶當成腰帶,系在腰上。
比如這樣:
然後收件人將這條皮帶纏繞在相同的木棒上,就能恢復資訊了。
前404年,一位遍體鱗傷的信差來到斯巴達將領利桑德面前,這趟波斯之旅只有他和四位同伴倖存,利桑德接下腰帶,纏繞到他的密碼棒上,得知波斯的發那巴祖斯準備侵襲他,多虧密碼棒利桑德才能夠預先防範,擊退敵軍。
2、頻率分析解密法
密碼棒是不是太簡單了些?
加密者選擇將組成資訊的字母替代成別的字母,比如說將a寫成1,這樣就不能被解密者直接拿到資訊了。
這難不倒解密者,以英文字母為例,為了確定每個英文字母的出現頻率,分析一篇或者數篇普通的英文文章,英文字母出現頻率最高的是e,接下來是t,然後是a……,然後檢查要破解的密文,也將每個字母出現的頻率整理出來,假設密文中出現頻率最高的字母是j,那麼就可能是e的替身,如果密碼文中出現頻率次高的但是P,那麼可能是t的替身,以此類推便就能解開加密資訊的內容。這就是頻率分析法。
- 將明文字母的出現頻率與密文字母的頻率相比較的過程
- 通過分析每個符號出現的頻率而輕易地破譯代換式密碼
- 在每種語言中,冗長的文章中的字母表現出一種可對之進行分辨的頻率
- e是英語中最常用的字母,其出現頻率為八分之一
將 article.txt 拷貝到專案資料夾的根目錄,並進行加密和解密的測試。
Util.java
public class Util {
public static void print(byte[] bytes) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
sb.append(bytes[i]).append(" ");
}
System.out.println(sb);
}
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();
}
}
}
}
public static String inputStream2String(InputStream in) throws IOException {
int len = -1;
byte[] buffer = new byte[1024];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = in.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.close();
return baos.toString("UTF-8");
}
}
FrequencyAnalysis.java
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("article.txt");
//加密檔案
int key = 3;
encryptFile("article.txt", "article_en.txt", key);
//統計加密後的字元個數
printCharCount("article_en.txt");
//讀取加密後的檔案
String artile = Util.file2String("article_en.txt");
//解密(會生成多個備選檔案)
decryptCaesarCode(artile, "article_de.txt");
}
public static void printCharCount(String path) throws IOException {
String data = Util.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 = Util.file2String(srcFile);
//加密檔案
String encryptData = KaiserDemo.encryptKaiser(artile, key);
//儲存加密後的檔案
Util.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 = KaiserDemo.decryptKaiser(input, key);
String fileName = "de_" + deCount + destPath;
Util.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<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;
}
}
執行 FrequencyAnalysis.java 用來統計每個字元出現的次數
執行 FrequencyAnalysis.java 裡面 main 函式裡面的 encryptFile 方法 對程式進行加密
在根目錄會生成一個 article_en.txt 檔案,然後我們統計這個檔案當中每個字元出現的次數
我們來看看頻度分析法如何工作的
執行程式
執行結果 # 出現次數最多, 我們知道在英文當中 e 出現的頻率是最高的,我們假設現在 # 號,就是 e ,變形而來的 ,我們可以對照 ascii 編碼表 ,我們的凱撒加密當中位移是加了一個 key ,所以我們 猜測 兩個值直接相差 -66 ,我們現在就以 -66 進行解密 生成一個檔案,我們檢視第一個檔案發現,根本讀不懂,所以解密失敗,我們在猜測 h 是 e ,h 和 e 之間相差3 ,所以我們在去看第二個解密檔案,發現我們可以讀懂,解密成功
七、Byte和bit
Byte : 位元組. 資料儲存的基本單位,比如行動硬碟1T,單位是byte
bit : 位元, 又叫位. 一個位要麼是0要麼是1. 資料傳輸的單位 , 比如家裡的寬頻100MB,下載速度並沒有達到100MB,一般都是12-13MB,那麼是因為需要使用 100 / 8
關係: 1Byte = 8bit
1、獲取字串byte
public class ByteBit {
public static void main(String[] args) {
String a = "a";
byte[] bytes = a.getBytes();
for (byte b : bytes) {
System.out.println("b = " + b);
int c = b;
// 打印發現byte實際上就是ascii碼
System.out.println(c);
}
}
}
執行結果:
2、byte對應bit
public class ByteBit {
public static void main(String[] args) {
String a = "a";
byte[] bytes = a.getBytes();
for (byte b : bytes) {
System.out.println("b = " + b);
int c = b;
// 打印發現byte實際上就是ascii碼
System.out.println(c);
// 我們在來看看每個byte對應的bit,byte獲取對應的bit
String bitStr = Integer.toBinaryString(c);
System.out.println(bitStr);
}
}
}
執行程式
打印出來應該是8個bit,但前面是0,沒有列印 ,從列印結果可以看出來,一個英文字元 ,佔一個位元組
其實就是ASCII碼對應的二進位制資料。
3、中文對應的位元組
// 中文在GBK編碼下, 佔據2個位元組
// 中文在UTF-8編碼下, 佔據3個位元組
public class ByteBitDemo {
public static void main(String[] args) throws Exception{
String a = "尚";
byte[] bytes = a.getBytes();
for (byte b : bytes) {
System.out.print(b + " ");
//獲取對應的 bit
String s = Integer.toBinaryString(b);
System.out.println(s);
}
}
}
執行程式:我們發現一箇中文是有 3 個位元組組成
我們修改編碼格式, 編碼格式改成GBK,我們在執行發現變成了 2 個位元組
// 中文在GBK編碼下, 佔據2個位元組
// 中文在UTF-8編碼下, 佔據3個位元組
public class ByteBitDemo {
public static void main(String[] args) throws Exception{
String a = "尚";
// 在中文情況下,不同的編碼格式,對應不同的位元組
//GBK :編碼格式佔2個位元組
// UTF-8:編碼格式佔3個位元組
byte[] bytes = a.getBytes("GBK");
// byte[] bytes = a.getBytes("UTF-8");
//byte[] bytes = a.getBytes(); 預設使用的是 UTF-8
for (byte b : bytes) {
System.out.print(b + " ");
//獲取對應的 bit
String s = Integer.toBinaryString(b);
System.out.println(s);
}
}
}
執行程式
4、英文對應的位元組
我們在看看英文,在不同的編碼格式佔用多少位元組
public static void main(String[] args) throws Exception {
String a = "A";
byte[] bytes = a.getBytes();
// 在中文情況下,不同的編碼格式,對應不同的位元組
//byte[] bytes = a.getBytes("GBK");
for (byte b : bytes) {
System.out.print(b + " ");
String s = Integer.toBinaryString(b);
System.out.println(s);
}
}
執行程式:
可以發現在英文情況下,不同的編碼格式都只佔用了一個位元組。