一文讀懂 MD5 算法

1、什麼是消息摘要

消息摘要算法是密碼學算法中很是重要的一個分支,它經過對全部數據提取指紋信息以實現數據簽名、數據完整性校驗等功能,因爲其不可逆性,有時候會被用作敏感信息的加密。消息摘要算法也被稱爲哈希(Hash)算法或散列算法。javascript

任何消息通過散列函數處理後,都會得到惟一的散列值,這一過程稱爲 「消息摘要」,其散列值稱爲 「數字指紋」,其算法天然就是 「消息摘要算法」了。換句話說,若是其數字指紋一致,就說明其消息是一致的。html

hash-function.png

(圖片來源 —— https://zh.wikipedia.org/wiki...java

消息摘要算法的主要特徵是加密過程不須要密鑰,而且通過加密的數據沒法被解密,目前能夠解密逆向的只有 CRC32 算法,只有輸入相同的明文數據通過相同的消息摘要算法才能獲得相同的密文。消息摘要算法不存在密鑰的管理與分發問題,適合於分佈式網絡上使用。消息摘要算法主要應用在 「數字簽名」 領域,做爲對明文的摘要算法。著名的摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的變體。node

1.1 消息摘要算法的特色

  • 不管輸入的消息有多長,計算出來的消息摘要的長度老是固定的。例如應用 MD5 算法摘要的消息有 128 個比特位,用 SHA-1 算法摘要的消息最終有 160 個比特位的輸出,SHA-1的變體能夠產生 192 個比特位和 256 個比特位的消息摘要。通常認爲,摘要的最終輸出越長,該摘要算法就越安全。
  • 消息摘要看起來是 「隨機的」。這些比特看上去是胡亂的雜湊在一塊兒的,能夠用大量的輸入來檢驗其輸出是否相同,通常,不一樣的輸入會有不一樣的輸出,並且輸出的摘要消息能夠經過隨機性檢驗。通常地,只要輸入的消息不一樣,對其進行摘要之後產生的摘要消息也必不相同;但相同的輸入必會產生相同的輸出。
  • 消息摘要函數是單向函數,即只能進行正向的信息摘要,而沒法從摘要中恢復出任何的消息,甚至根本就找不到任何與原信息相關的信息。
  • 好的摘要算法,沒有人能從中找到 「碰撞」 或者說極度難找到,雖然 「碰撞」 是確定存在的(碰撞即不一樣的內容產生相同的摘要)。

2、什麼是 MD5 算法

MD5(Message Digest Algorithm 5,消息摘要算法版本5),它由 MD二、MD三、MD4 發展而來,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被普遍應用於數據完整性校驗、數據(消息)摘要、數據簽名等。MD二、MD四、MD5 都產生 16 字節(128 位)的校驗值,通常用 32 位十六進制數表示。MD2 的算法較慢但相對安全,MD4 速度很快,但安全性降低,MD5 比 MD4 更安全、速度更快。mysql

隨着計算機技術的發展和計算水平的不斷提升,MD5 算法暴露出來的漏洞也愈來愈多。1996 年後被證明存在弱點,能夠被加以破解,對於須要高度安全性的數據,專家通常建議改用其餘算法,如 SHA-2。2004 年,證明 MD5 算法沒法防止碰撞(collision),所以不適用於安全性認證,如 SSL 公開密鑰認證或是數字簽名等用途。git

2.1 MD5 特色

  • 穩定、運算速度快。
  • 壓縮性:輸入任意長度的數據,輸出長度固定(128 比特位)。
  • 運算不可逆:已知運算結果的狀況下,沒法經過經過逆運算獲得原始字符串。
  • 高度離散:輸入的微小變化,可致使運算結果差別巨大。

2.2 MD5 散列

128 位的 MD5 散列在大多數狀況下會被表示爲 32 位十六進制數字。如下是一個 43 位長的僅 ASCII 字母列的MD5 散列:github

MD5("The quick brown fox jumps over the lazy dog")
= 9e107d9d372bb6826bd81d3542a419d6

即便在原文中做一個小變化(好比把 dog 改成 cog,只改變一個字符)其散列也會發生巨大的變化:算法

MD5("The quick brown fox jumps over the lazy cog")
= 1055d3e698d289f2af8663725127bd4b

接着咱們再來舉幾個 MD5 散列的例子:sql

MD5("") -> d41d8cd98f00b204e9800998ecf8427e 
MD5("semlinker") -> 688881f1c8aa6ffd3fcec471e0391e4d
   MD5("kakuqo") -> e18c3c4dd05aef020946e6afbf9e04ef

3、MD5 算法的用途

3.1 防止被篡改

3.1.1 文件分發防篡改shell

在互聯網上分發軟件安裝包時,出於安全性考慮,爲了防止軟件被篡改,好比在軟件安裝程序中添加木馬程序。軟件開發者一般會使用消息摘要算法,好比 MD5 算法產生一個與文件匹配的數字指紋,這樣接收者在接收到文件後,就能夠利用一些現成的工具來檢查文件完整性。

cpt-hashing-file-transmission.png

(圖片來源 —— https://en.wikipedia.org/wiki...

這裏咱們來舉一個實際的例子,下圖是 MySQL Community Server 8.0.19 版本的下載頁,該下載頁經過 MD5 算法分別計算出不一樣軟件包的數字指紋,具體以下圖所示:

mysql-8.0.19-md5.jpg

(圖片來源 —— https://dev.mysql.com/downloa...

當用戶從官網上下載到對應的安裝包以後,能夠利用一些 MD5 校驗工具對已下載的文件進行校驗,而後比對最終的 MD5 數字指紋,若結果與官網公佈的數字指紋一致,則表示該安裝包未通過任何修改是安全的,基本能夠放心安裝。

3.1.2 消息傳輸防篡改

假設在網絡上你須要發送電子文檔給你的朋友,在文件發送前,先對文檔的內容進行 MD5 運算,得出該電子文檔的 「數字指紋」,並把該 「數字指紋」 隨電子文檔一同發送給對方。當對方接收到電子文檔以後,也使用 MD5 算法對文檔的內容進行哈希運算,在運算完成後也會獲得一個對應 「數字指紋」,當該指紋與你所發送文檔的 「數字指紋」 一致時,表示文檔在傳輸過程當中未被篡改。

3.2 信息保密

在互聯網初期不少網站在數據庫中以明文的形式存儲用戶的密碼,這存在很大的安全隱患,好比數據庫被黑客入侵,從而致使網站用戶信息的泄露。針對這個問題,一種解決方案是在保存用戶密碼時,再也不使用明文,而是使用消息摘要算法,好比 MD5 算法對明文密碼進行哈希運算,而後把運算的結果保存到數據庫中。使用上述方案,避免了在數據庫中以明文方式保存密碼,提升了系統的安全性,不過這種方案並不安全,後面咱們會詳細分析。

password-md5.jpg

當用戶登陸時,登陸系統對用戶輸入的密碼執行 MD5 哈希運算,而後再使用用戶 ID 和密碼對應的 MD5 「數字指紋」 進行用戶認證。若認證經過,則當前的用戶能夠正常登陸系統。用戶密碼通過 MD5 哈希運算後存儲的方案至少有兩個好處:

  • 防內部攻擊:由於在數據庫中不會以明文的方式保存密碼,所以能夠避免系統中用戶的密碼被具備系統管理員權限的人員知道。
  • 防外部攻擊:網站數據庫被黑客入侵,黑客只能獲取通過 MD5 運算後的密碼,而不是用戶的明文密碼。

4、MD5 算法使用示例

4.1 Java 示例

在 Java 中使用 MD5 算法很方便,能夠直接使用 JDK 自帶的 MD5 實現,也可使用第三方庫提供的 MD5 實現。下面咱們將介紹 JDK、Bouncy Castle 和 Guava 的 MD5 使用示例。爲了保證如下示例的正常運行,首先咱們須要在 pom.xml 文件中添加 Bouncy Castle 和 Guava 的座標:

<dependency>
     <groupId>org.bouncycastle</groupId>
     <artifactId>bcprov-jdk15on</artifactId>
     <version>1.64</version>
</dependency>
<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>27.1-jre</version>
</dependency>

JDK 實現

public static void jdkMD5(String src) throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("MD5");
    byte[] md5Bytes = md.digest(src.getBytes());
    System.out.println("JDK MD5:" + src + " -> " + bytesToHexString(md5Bytes));
}

Bouncy Castle 實現

public static void bcMD5(String src) {
    MD5Digest digest = new MD5Digest();
    digest.update(src.getBytes(), 0, src.getBytes().length);
    byte[] md5Bytes = new byte[digest.getDigestSize()];
    digest.doFinal(md5Bytes, 0);
    System.out.println("Bouncy Castle MD5:" + src + " -> " + 
      bytesToHexString(md5Bytes));
}

Guava 實現

public static void guavaMD5(String src) {
    HashFunction hf = Hashing.md5();
    HashCode hc = hf.newHasher().putString(src, Charset.defaultCharset()).hash();
    System.out.println("Guava MD5:" + src + " -> " + hc);
}

在 JDK 實現和 Bouncy Castle 實現的示例中使用了 bytesToHexString 方法,該方法用於把字節數組轉換成十六進制,它的具體實現以下:

private static String bytesToHexString(byte[] src) {
    StringBuilder stringBuilder = new StringBuilder();
    if (src == null || src.length <= 0) {
         return null;
    }
    for (int i = 0; i < src.length; i++) {
        int v = src[i] & 0xFF;
        String hv = Integer.toHexString(v);
        if (hv.length() < 2) {
            stringBuilder.append(0);
        }
        stringBuilder.append(hv);
    }
    return stringBuilder.toString();
}

介紹完 MD5 算法不一樣的實現,下面咱們來測試一下上述的方法:

public static void main(String[] args) throws NoSuchAlgorithmException {
   jdkMD5("123");
   bcMD5("123");
   guavaMD5("123");
}

以上示例代碼正常運行後,在控制檯中會輸出如下結果:

JDK MD5:123 -> 202cb962ac59075b964b07152d234b70
Bouncy Castle MD5:123 -> 202cb962ac59075b964b07152d234b70
Guava MD5:123 -> 202cb962ac59075b964b07152d234b70

4.2 Node.js 示例

在 Node.js 環境中,咱們可使用 crypto 原生模塊提供的 md5 實現,固然也可使用主流的 MD5 第三方庫,好比 md5 這個能夠同時運行在服務端和客戶端的第三方庫。與 Java 示例同樣,在介紹具體使用前,咱們須要提早安裝 md5 這個第三方庫,具體安裝方式以下:

$ npm install md5 --save

Node.js Crypto 實現

const crypto = require('crypto'); 

const msg = "123";
function md5(data){
  const hash = crypto.createHash('md5');
  return hash.update(data).digest('hex');
}

console.log("Node.js Crypto MD5:" + msg + " -> " + md5(msg));

Node.js MD5 第三方庫實現

const md5 = require('md5');
const msg = "123";
console.log("MD5 Lib MD5:" + msg + " -> " + md5(msg));

以上示例代碼正常運行後,在控制檯中會輸出如下結果:

Node.js Crypto MD5:123 -> 202cb962ac59075b964b07152d234b70
MD5 Lib MD5:123 -> 202cb962ac59075b964b07152d234b70

5、MD5 算法的缺陷

哈希碰撞是指不一樣的輸入卻產生了相同的輸出,好的哈希算法,應該沒有人能從中找到 「碰撞」 或者說極度難找到,雖然 「碰撞」 是確定存在的。

2005 年山東大學的王小云教授發佈算法能夠輕易構造 MD5 碰撞實例,此後 2007 年,有國外學者在王小云教授算法的基礎上,提出了更進一步的 MD5 前綴碰撞構造算法 「chosen prefix collision」,此後還有專家陸續提供了MD5 碰撞構造的開源的庫。

2009 年,中國科學院的謝濤和馮登國僅用了 220.96 的碰撞算法複雜度,破解了 MD5 的碰撞抵抗,該攻擊在普通計算機上運行只須要數秒鐘。

MD5 碰撞很容易構造,基於 MD5 來驗證數據完整性已不可靠,考慮到近期谷歌已成功構造了 SHA-1(英語:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞實例,對於數據完整性,應使用 SHA256 或更強的算法代替。

下面咱們來看個簡單的 MD5 碰撞示例:

HEX(十六進制)樣本A1

4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa200a8284bf36e8e4b55b35f4275
93d849676da0d1555d8360fb5f07fea2

HEX(十六進制)樣本A2

4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa202a8284bf36e8e4b55b35f4275
93d849676da0d1d55d8360fb5f07fea2

兩個樣本之間的差別以下圖所示:

md5-collision-sample.jpg

下面咱們來經過 Java 代碼實際驗證一下樣本 A1 和樣本 A2 通過 MD5 運算後輸出的結果是否一致:

jdkMd5Hex 方法

public static void jdkMd5Hex(String hexStr) throws NoSuchAlgorithmException {
    byte[] bytes = hexStringToBytes(hexStr);
    MessageDigest md = MessageDigest.getInstance("MD5");
    byte[] md5Bytes = md.digest(bytes);
    System.out.println("JDK MD5:" + hexStr + " -> " + bytesToHexString(md5Bytes));
}

hexStringToBytes 方法

public static byte[] hexStringToBytes(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
          + Character.digit(s.charAt(i+1), 16));
    }
    return data;
}

main 方法

public static void main(String[] args) throws NoSuchAlgorithmException {
    jdkMd5Hex("4dc968ff..."); //樣本A
    jdkMd5Hex("4dc968ff..."); //樣本B
}

以上示例代碼正常運行後,在控制檯中會輸出如下結果:

JDK MD5:4dc968ff... -> 008ee33a9d58b51cfeb425b0959121c9
JDK MD5:4dc968ff... -> 008ee33a9d58b51cfeb425b0959121c9

若是你對其它 MD5 碰撞的樣本感興趣,能夠查看 MD5碰撞的一些例子 這篇文章。因爲基於 MD5 來驗證數據完整性已不可靠,所以不少人都熟悉的 Node.js 使用了 SHA256 算法來確保數據的完整性。

nodejs-dist-sha256.jpg

(圖片來源 —— https://nodejs.org/dist/v12.1...

6、MD5 密碼安全性

6.1 MD5 密文反向查詢

前面咱們已經提到經過對用戶密碼進行 MD5 運算能夠提升系統的安全性。但實際上,這樣的安全性仍是不高。爲何呢?由於只要輸入相同就會產生相同的輸出。接下來咱們來舉一個示例,字符串 123456789 是一個很經常使用的密碼,它通過 MD5 運算後會生成一個對應的哈希值:

MD5("123456789") -> 25f9e794323b453885f5181f1b624d0b

因爲輸入相同就會產生相同的結果,所以攻擊者就能夠根據哈希結果反推輸入。其中一種常見的破解方式就是使用彩虹表。彩虹表是一個用於加密散列函數逆運算的預先計算好的表,經常使用於破解加密過的密碼散列。 查找表經常用於包含有限字符固定長度純文本密碼的加密。這是以空間換時間的典型實踐,在每一次嘗試都計算的暴力破解中使用更少的計算能力和更多的儲存空間,但卻比簡單的每一個輸入一條散列的翻查表使用更少的儲存空間和更多的計算性能。

目前網上某些站點,好比 cmd5.com 已經爲咱們提供了 MD5 密文的反向查詢服務,咱們以 MD5("123456789") 生成的結果,作個簡單的驗證,具體以下圖所示:

cmd5-com-demo.jpg

由於 123456789 是很常見的密碼,所以該網站可以反向得出正確結果那就不足爲奇了。如下是 cmd5 網站的站點說明,你們能夠參考一下,感興趣的小夥伴能夠親自驗證一下。

本站針對 md五、sha1 等全球通用公開的加密算法進行反向查詢,經過窮舉字符組合的方式,建立了明文密文對應查詢數據庫,建立的記錄約 90 萬億條,佔用硬盤超過 500 TB,查詢成功率 95% 以上,不少複雜密文只有本站纔可查詢。已穩定運行十餘年,國內外享有盛譽。

如今咱們已經知道若是用戶的密碼相同 MD5 的值就會同樣,經過一些 MD5 密文的反向查詢網站,密碼大機率會被解析出來,這樣使用相同密碼的用戶就會受到影響。那麼該問題如何解決呢?答案是密碼加鹽。

6.2 密碼加鹽

鹽(Salt),在密碼學中,是指在散列以前將散列內容(例如:密碼)的任意固定位置插入特定的字符串。這個在散列中加入字符串的方式稱爲 「加鹽」。其做用是讓加鹽後的散列結果和沒有加鹽的結果不相同,在不一樣的應用情景中,這個處理能夠增長額外的安全性。

在大部分狀況,鹽是不須要保密的。鹽能夠是隨機產生的字符串,其插入的位置能夠也是隨意而定。若是這個散列結果在未來須要進行驗證(例如:驗證用戶輸入的密碼),則須要將已使用的鹽記錄下來。爲了便於理解,咱們來舉個簡單的示例。

Node.js MD5 加鹽示例

const crypto = require("crypto");

function cryptPwd(password, salt) {
  const saltPassword = password + ":" + salt;
  console.log("原始密碼:%s", password);
  console.log("加鹽後的密碼:%s", saltPassword);

  const md5 = crypto.createHash("md5");
  const result = md5.update(saltPassword).digest("hex");
  console.log("加鹽密碼的md5值:%s", result);
}

cryptPwd("123456789","exe");
cryptPwd("123456789","eft");

以上示例代碼正常運行後,在控制檯中會輸出如下結果:

原始密碼:123456789
加鹽後的密碼:123456789:exe
加鹽密碼的md5值:3328003d9f786897e0749f349af490ca
原始密碼:123456789
加鹽後的密碼:123456789:eft
加鹽密碼的md5值:3c45dd21ba03e8216d56dce8fe5ebabf

經過觀察以上結果,咱們發現原始密碼一致,但使用的鹽值不同,最終生成的 MD5 哈希值差別也比較大。此外爲了提升破解的難度,咱們能夠隨機生成鹽值而且提升鹽值的長度。

6.3 bcrypt

哈希加鹽的方式確實可以增長攻擊者的成本,可是今天來看還遠遠不夠,咱們須要一種更加安全的方式來存儲用戶的密碼,這也就是今天被普遍使用的 bcrypt

bcrypt 是一個由 Niels Provos 以及 David Mazières 根據 Blowfish 加密算法所設計的密碼散列函數,於 1999 年在 USENIX 中展現。bcrypt 這一算法就是爲哈希密碼而專門設計的,因此它是一個執行相對較慢的算法,這也就可以減小攻擊者每秒可以處理的密碼數量,從而避免攻擊者的字典攻擊。實現中 bcrypt 會使用一個加鹽的流程以防護彩虹表攻擊,同時 bcrypt 仍是適應性函數,它能夠藉由增長迭代之次數來抵禦日益增進的電腦運算能力透過暴力法破解。

由 bcrypt 加密的文件可在全部支持的操做系統和處理器上進行轉移。它的口令必須是 8 至 56 個字符,並將在內部被轉化爲 448 位的密鑰。然而,所提供的全部字符都具備十分重要的意義。密碼越強大,您的數據就越安全。

下面咱們以 Node.js 平臺的 bcryptjs 爲例,介紹一下如何使用 bcrypt 算法來處理用戶密碼。首先咱們須要先安裝 bcryptjs

$ npm install bcryptjs --save

Node.js bcryptjs 處理密碼

const bcrypt = require("bcryptjs");

const password = "123456789";
const saltRounds = 10;

async function bcryptHash(str, saltRounds) {
  let hashedResult;
  try {
    const salt = await bcrypt.genSalt(saltRounds);
    hashedResult = await bcrypt.hash(str, salt);
  } catch (error) {
    throw error;
  }
  return hashedResult;
}

bcryptHash(password, saltRounds).then(console.log);

以上示例代碼正常運行後,在控制檯中會輸出如下結果:

$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm

很明顯密碼 123456789 通過 bcrypt 的哈希運算後,獲得了一串讀不懂的 「亂碼」。這裏咱們已經完成第一步,即用戶登陸密碼的加密。下一步咱們要實現登陸密碼的比對,即要保證用戶輸入正確的密碼後,能正常登陸系統。

Node.js bcryptjs 密碼校驗

async function bcryptCompare(str, hashed) {
  let isMatch;
  try {
    isMatc = await bcrypt.compare(str, hashed);
  } catch (error) {
    throw error;
  }
  return isMatch;
}

bcryptCompare(
  "123456789",
  "$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm"
).then(console.log);

bcryptCompare(
  "123456",
  "$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm"
).then(console.log);

以上示例代碼正常運行後,在控制檯中會輸出如下結果:

true
false

由於咱們的原始密碼是 123456789,很明顯與 123456 並不匹配,因此會輸出以上的匹配結果。

7、總結

本文首先介紹了消息摘要算法、MD5 算法的相關概念和特色,而後詳細介紹了 MD5 算法的用途和 Java 和 Node.js 平臺的使用示例,最後咱們還分析了 MD5 算法存在的缺陷和 MD5 密碼的安全性問題。這裏你們須要注意,因爲 MD5 碰撞很容易構造,基於 MD5 來驗證數據完整性已不可靠,考慮到近期谷歌已成功構造了 SHA-1(英語:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞實例,對於數據完整性,應使用 SHA256 或更強的算法代替。

除了文中介紹的 MD5 應用場景,MD5 還能夠用於實現 CDN (Content Delivery Network,內容分發網絡) 內容資源的防盜鏈,感興趣的小夥伴能夠閱讀 深刻了解 Token 防盜鏈 這篇文章。

8、參考資源

本人的全棧修仙之路訂閱號,會按期分享 Angular、TypeScript、Node.js/Java 、Spring 相關文章,歡迎感興趣的小夥伴訂閱哈!

full-stack-logo

相關文章
相關標籤/搜索