1. 程式人生 > 其它 >第一章:密碼學發展歷史與基礎知識

第一章:密碼學發展歷史與基礎知識

一、密碼學基本概念

密碼在我們的生活中有著重要的作用,那麼密碼究竟來自何方,為何會產生呢?

密碼學是網路安全、資訊保安、區塊鏈等產品的基礎,常見的非對稱加密、對稱加密、雜湊函式等,都屬於密碼學範疇。

密碼學有數千年的歷史,從最開始的替換法到如今的非對稱加密演算法,經歷了古典密碼學,近代密碼學和現代密碼學三個階段。密碼學不僅僅是數學家們的智慧,更是如今網路空間安全的重要基礎。

二、密碼學的發展歷史

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);
        }
    }

執行程式:

可以發現在英文情況下,不同的編碼格式都只佔用了一個位元組。