聊聊主流加密算法及該如何設計我們的用戶密碼

  • 2019 年 10 月 3 日
  • 筆記

主流加密算法

對稱加密

對稱加密指加密和解密使用相同密鑰的加密算法,有時又叫傳統密碼算法而在大多數的對稱算法中,加密密鑰和解密密鑰是相同的,所以也稱這種加密算法為秘密密鑰算法或單密鑰算法。它要求發送方和接收方在安全通信之前,商定一個密鑰。對稱算法的安全性依賴於密鑰,泄漏密鑰就意味着任何人都可以對他們發送或接收的消息解密,所以密鑰的保密性對通信性至關重要。對稱加密算法的優點是算法公開、計算量小、加密速度快、加密效率高。常見的對稱加密算法有:

1.DES

DES全稱為Data Encryption Standard,即數據加密標準,是一種使用密鑰加密的塊算法,1976年被美國聯邦政府的國家標準局確定為聯邦資料處理標準(FIPS),隨後在國際上廣泛流傳開來。DES算法的入口參數有三個:Key、Data、Mode。其中Key為7個位元組共56位,是DES算法的工作密鑰;Data為8個位元組64位,是要被加密或被解密的數據;Mode為DES的工作方式,有兩種:加密或解密。

2.3DES

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

3.Blowfish

Blowfish算法是一個64位分組及可變密鑰長度的對稱密鑰分組密碼算法,可用來加密64比特長度的字符串。32位處理器誕生後,Blowfish算法因其在加密速度上超越了DES而引起人們的關注。Blowfish算法具有加密速度快、緊湊、密鑰長度可變、可免費使用等特點,已被廣泛使用於眾多加密軟件。

4.國際數據加密算法(IDEA)

IDEA是International Data Encryption Algorithm的縮寫,即國際數據加密算法,它的原型是1990年由瑞士聯邦技術學院X.J.Lai和Massey提出的PES。1992年,Lai和Massey對PES進行了改進和強化,產生了IDEA。這是一個非常成功的分組密碼,並且廣泛的應用在安全電子郵件PGP中

5.RC5

RC5是一種因簡潔著稱的對稱分組加密算法。由羅納德·李維斯特於1994年設計,「RC」代表「Rivest Cipher」,或者「Ron’s Code」(相較於RC2和RC4)。高級加密標準(AES)的候選算法之一RC6是基於RC5的。

6.AES

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

非對稱加密

公開密鑰密碼學(英語:Public-key cryptography,也稱非對稱式密碼學)是密碼學的一種算法,它需要兩個密鑰,一個是公開密鑰,另一個是私有密鑰;一個用作加密,另一個則用作解密。使用其中一個密鑰把明文加密後所得的密文,只能用相對應的另一個密鑰才能解密得到原本的明文;甚至連最初用來加密的密鑰也不能用作解密。由於加密和解密需要兩個不同的密鑰,故被稱為非對稱加密;不同於加密和解密都使用同一個密鑰的對稱加密。雖然兩個密鑰在數學上相關,但如果知道了其中一個,並不能憑此計算出另外一個;因此其中一個可以公開,稱為公鑰,任意向外發佈;不公開的密鑰為私鑰,必須由用戶自行嚴格秘密保管,絕不透過任何途徑向任何人提供,也不會透露給被信任的要通信的另一方。

1.RSA算法

RSA公鑰加密算法是1977年由羅納德·李維斯特(Ron Rivest)、阿迪·薩莫爾(Adi Shamir)和倫納德·阿德曼(Leonard Adleman)一起提出的。當時他們三人都在麻省理工學院工作。RSA就是他們三人姓氏開頭字母拼在一起組成的。
RSA是目前最有影響力的公鑰加密算法,它能夠抵抗到目前為止已知的絕大多數密碼攻擊,已被ISO推薦為公鑰數據加密標準。

點擊查看阮一峰對RSA算法的介紹

2.DSA

DSA(Digital Signature Algorithm)是Schnorr和ElGamal簽名算法的變種,被美國NIST作為DSS(DigitalSignature Standard)。
DSA加密算法主要依賴於整數有限域離散對數難題,素數P必須足夠大,且p-1至少包含一個大素數因子以抵抗Pohlig &Hellman算法的攻擊。M一般都應採用信息的HASH值。DSA加密算法的安全性主要依賴於p和g,若選取不當則簽名容易偽造,應保證g對於p-1的大素數因子不可約。其安全性與RSA相比差不多。
DSA 一般用於數字簽名和認證。在DSA數字簽名和認證中,發送者使用自己的私鑰對文件或消息進行簽名,接受者收到消息後使用發送者的公鑰來驗證簽名的真實性。DSA只是一種算法,和RSA不同之處在於它不能用作加密和解密,也不能進行密鑰交換,只用於簽名,它比RSA要快很多

3.ECC橢圓曲線加密算法

橢圓曲線加密算法(ECC) 是基於橢圓曲線數學的一種公鑰加密的算法,隨着分解大整數方法的進步以及完善、計算機計算速度的提高以及網絡的發展,RSA的使用率越來越高.但是為了安全。其密鑰的長度一直保守詬病,於是ECC這種新算法逐步走上了現在加密算法的這個大舞台,其使用率和重要性都在逐年上升

4.MD5

MD5消息摘要算法(英語:MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函數,可以產生出一個128位(16位元組)的散列值(hash value),用於確保信息傳輸完整一致。MD5由美國密碼學家羅納德·李維斯特(Ronald Linn Rivest)設計,於1992年公開,用以取代MD4算法。這套算法的程序在 RFC 1321 中被加以規範。

將數據(如一段文字)運算變為另一固定長度值,是散列算法的基礎原理。

1996年後被證實存在弱點,可以被加以破解,對於需要高度安全性的數據,專家一般建議改用其他算法,如SHA-2。2004年,證實MD5算法無法防止碰撞(collision),因此不適用於安全性認證,如SSL公開密鑰認證或是數字簽名等用途。

5.Bcrypt

bcrypt是一個由Niels Provos以及David Mazières根據Blowfish加密算法所設計的密碼散列函數,於1999年在USENIX中展示。實現中bcrypt會使用一個加鹽的流程以防禦彩虹表攻擊,同時bcrypt還是適應性函數,它可以藉由增加迭代之次數來抵禦日益增進的計算機運算能力透過暴力法破解。

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

除了對您的數據進行加密,默認情況下,bcrypt在刪除數據之前將使用隨機數據三次覆蓋原始輸入文件,以阻撓可能會獲得您的計算機數據的人恢複數據的嘗試。如果您不想使用此功能,可設置禁用此功能。

具體來說,bcrypt使用保羅·柯切爾的算法實現。隨bcrypt一起發佈的源代碼對原始版本作了略微改動。

6.scrypt

在密碼學中,scrypt(念作「ess crypt」)是Colin Percival於2009年所發明的金鑰推衍函數,當初設計用在他所創立的Tarsnap服務上。設計時考慮到大規模的客制硬件攻擊而刻意設計需要大量內存運算。2016年,scrypt算法發佈在RFC 7914。scrypt的簡化版被用在數個密碼貨幣的工作量證明(Proof-of-Work)上。

7.PBKDF2

PBKDF2,PBKDF2簡單而言就是將salted hash進行多次重複計算,這個次數是可選擇的。如果計算一次所需要的時間是1微秒,那麼計算1百萬次就需要1秒鐘。假如攻擊一個密碼所需的rainbow table有1千萬條,建立所對應的rainbow table所需要的時間就是115天。這個代價足以讓大部分的攻擊者忘而生畏。

8.SHA-2

SHA-2,名稱來自於安全散列算法2(英語:Secure Hash Algorithm 2)的縮寫,一種密碼散列函數算法標準,由美國國家安全局研發,由美國國家標準與技術研究院(NIST)在2001年發佈。屬於SHA算法之一,是SHA-1的後繼者。其下又可再分為六個不同的算法標準,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。

總結

不用明文存儲密碼,程序員們早在 n 多年前就已經達成了共識。不能明文存儲,一些 hash 算法便被廣泛用做密碼的編碼器,對密碼進行單向 hash 處理後存儲數據庫,當用戶登錄時,計算用戶輸入的密碼的 hash 值,將兩者進行比對。單向 hash 算法,顧名思義,它無法(或者用不能輕易更為合適)被反向解析還原出原密碼。這杜絕了管理員直接獲取密碼的途徑,可僅僅依賴於普通的 hash 算法(如 md5,sha256)是不合適的,他主要有 3 個特點:

  • 1.同一密碼生成的 hash 值一定相同
  • 2.不同密碼生成的 hash 值可能相同(md5 的碰撞問題相比 sha256 還要嚴重)
  • 3.生成簽名速度快

以上三點結合在一起,破解此類算法成了不是那麼困難的一件事,因此不適用於安全性認證,如SSL公開密鑰認證或是數字簽名等用途。

暴力破解

考慮到大多數用戶使用的密碼多為數字+字母+特殊符號的組合,攻擊者將常用的密碼進行枚舉,甚至通過排列組合來暴力破解,這被稱為 rainbow table。算法愛好者能夠立刻看懂到上述的方案,這被親切地稱之為—打表,一種暴力美學,這張表是可以被複用的。

加隨機鹽

加隨機鹽,使得被破解難度增加,雖然破解加隨機鹽的密碼難度增加,但在現在雲計算,超級計算服務流行的時代,卻並非不容易破解。

慢哈希算法

Bcrypt,Scrypt,PBKDF2 這些慢 hash 算法成為了設計用戶加密算法的主流,尤其是Bcrypt 算法被發明至今 19 年,使用範圍廣,且從未因為安全問題而被修改,其有限性是已經被驗證過的,相比之下新的哈希算法Scrypt,沒有 Bcrypt 使用的廣泛。從破解成本和權威性的角度來看,Bcrypt 用作密碼編碼器是不錯的選擇。

Spring Security 5.x的PasswordEncoderFactories解析

回到Spring Boot2,PasswordEncoderFactories是Spring Security創建DelegatingPasswordEncoder對象的工廠類。該工廠所創建的DelegatingPasswordEncoder默認使用bcrypt算法用於加密,並且能夠用於匹配以下幾種密碼類型 :

  • ldap
  • MD4
  • MD5
  • noop (明文密碼)
  • pbkdf2
  • scrypt
  • SHA-1
  • SHA-256
  • sha256

源代碼

    /**       * Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional       * mappings may be added and the encoding will be updated to conform with best       * practices. However, due to the nature of {@link DelegatingPasswordEncoder} the       * updates should not impact users. The mappings current are:       *       * <ul>       * <li>bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)</li>       * <li>ldap - {@link org.springframework.security.crypto.password.LdapShaPasswordEncoder}</li>       * <li>MD4 - {@link org.springframework.security.crypto.password.Md4PasswordEncoder}</li>       * <li>MD5 - {@code new MessageDigestPasswordEncoder("MD5")}</li>       * <li>noop - {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}</li>       * <li>pbkdf2 - {@link Pbkdf2PasswordEncoder}</li>       * <li>scrypt - {@link SCryptPasswordEncoder}</li>       * <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li>       * <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li>       * <li>sha256 - {@link org.springframework.security.crypto.password.StandardPasswordEncoder}</li>       * </ul>       *       * @return the {@link PasswordEncoder} to use       */      @SuppressWarnings("deprecation")      public static PasswordEncoder createDelegatingPasswordEncoder() {          String encodingId = "bcrypt";          Map<String, PasswordEncoder> encoders = new HashMap<>();          encoders.put(encodingId, new BCryptPasswordEncoder());          encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());          encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());          encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));          encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());          encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());          encoders.put("scrypt", new SCryptPasswordEncoder());          encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));          encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));          encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());            return new DelegatingPasswordEncoder(encodingId, encoders);      }

創建 DelegatingPasswordEncoder 實例的工廠方法,encodingId為默認加密算法,將Spring Security提供的所有PasswordEncoder實現都包裝到所創建的DelegatingPasswordEncoder中,當用於匹配密碼時,密碼密文的格式是 : "{encoderId}abc",DelegatingPasswordEncoder會解析出密碼中的"encoderId"從encoders map中找到相應的PasswordEncoder去檢驗輸入的密碼和密碼密文中的"abc"是否匹配

示例代碼

@Slf4j  public class PasswordEncoderFactoriesTest {      public static void main(String[] args) {          String password = "123456";          PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();            log.info("PasswordEncoderFactories默認加密算法是bcrypt");          String encodeStr1 = passwordEncoder.encode(password);          String encodeStr2 = passwordEncoder.encode(password);          log.info("明文==>{},密文==>{},是否匹配==>{}", password, encodeStr1, passwordEncoder.matches(password, encodeStr1));          log.info("明文==>{},密文==>{},是否匹配==>{}", password, encodeStr2, passwordEncoder.matches(password, encodeStr2));            String[] encodes = {"bcrypt", "ldap", "MD4", "MD5", "noop", "pbkdf2", "scrypt", "SHA-1", "SHA-256", "sha256"};            log.info("=========================================批量測試=========================================");          List<String> encodeList = new ArrayList();          for (String encode : encodes) {              passwordEncoder = newPasswordEncoder(encode);              String encodeStr = passwordEncoder.encode(password);              encodeList.add(encodeStr);              log.info("{}算法,明文==>{},密文==>{}", encode, password, encodeStr);              log.info("密文和明文匹配 :{}", passwordEncoder.matches(password, encodeStr));          }      }        public static PasswordEncoder newPasswordEncoder(final String encoderType) {            switch (encoderType) {              case "bcrypt":                  return new BCryptPasswordEncoder();              case "ldap":                  return new org.springframework.security.crypto.password.LdapShaPasswordEncoder();              case "MD4":                  return new org.springframework.security.crypto.password.Md4PasswordEncoder();              case "MD5":                  return new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5");              case "noop":                  return org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance();              case "pbkdf2":                  return new Pbkdf2PasswordEncoder();              case "scrypt":                  return new SCryptPasswordEncoder();              case "SHA-1":                  return new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1");              case "SHA-256":                  return new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256");              case "sha256":                  return new org.springframework.security.crypto.password.StandardPasswordEncoder();              default:                  return NoOpPasswordEncoder.getInstance();          }      }  }

示例代碼控制台輸出

10:55:57.445 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - PasswordEncoderFactories默認加密算法是bcrypt  10:55:57.774 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 明文==>123456,密文==>{bcrypt}$2a$10$GyBTqqaxJ71LK8TruE4jqOkjgEfUVUSR9HpVKXHmPKHcj3sZRHJEq,是否匹配==>true  10:55:57.868 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 明文==>123456,密文==>{bcrypt}$2a$10$JXwZWyZ5agNLuABZm03bju7CY1a6oafu623bOfQOBAZvviVPw9/eS,是否匹配==>true  10:55:57.868 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - =========================================批量測試=========================================  10:55:57.977 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - bcrypt算法,明文==>123456,密文==>$2a$10$YzAS9Kt3lAmoKVP1XXqSnu8F8O/t.wu/7rhePtsS41ZactBPf5g5i  10:55:58.086 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:58.088 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - ldap算法,明文==>123456,密文==>{SSHA}6plLy61WbK9UQhn1CWdpCcXitaFadSZye/j25A==  10:55:58.089 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:58.091 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - MD4算法,明文==>123456,密文==>{Qzz1t9tzRfKL9pORm48J+7y5WixLz9FCKwhQIlwOqBI=}c86bf96ce1d0ede7b410c4ef9863359b  10:55:58.091 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:58.091 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - MD5算法,明文==>123456,密文==>{L0S2FjwL2tXdwotBho0fgUrIRpBaVMPgJsxUiSWtV9E=}7f8fb8fac4922ceed555f223bcdf8361  10:55:58.091 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:58.091 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - noop算法,明文==>123456,密文==>123456  10:55:58.091 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:58.903 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - pbkdf2算法,明文==>123456,密文==>b25a5d4b5a352d5506b05c37b89b53117c5952a4f8a28d06f60bc9eaecc7a9ebe0325f077f6ab6b7  10:55:59.355 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:59.545 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - scrypt算法,明文==>123456,密文==>$e0801$sy5MK3+yFPMdBHHUiheU6oUfYrTNAhdzRSp/qHkfHb6jTPOiCOpumILt1yKBbUDyPJu0gCo/40oTEI8mm9yxaw==$s3GapoqS1ZRsWiLBJBUKN2lazcovqgr/dP1yAEYCdvQ=  10:55:59.616 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:59.616 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - SHA-1算法,明文==>123456,密文==>{nXs3ooYFHHvv5mp/Dl/gHoeXwPdx2YWt9lZH3GeNXVg=}f5c986dddd9997626dffad2f9b4cad5e19fedef2  10:55:59.616 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:59.616 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - SHA-256算法,明文==>123456,密文==>{HmaQe0Qx9Tw3KKM6UoDxX9udUJtIiN4XD83qwDYunsg=}642c273f94b9ec30572f25cea0c958d7b62a48aa4527573b0e936b97cc762a4d  10:55:59.616 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true  10:55:59.618 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - sha256算法,明文==>123456,密文==>cbbf80ab6f00508cbd016da31cceb613205059b5e38b4a8ec24e7d0cc7bcb296ef7db11cba2e1409  10:55:59.620 [main] INFO com.easy.encoder.PasswordEncoderFactoriesTest - 密文和明文匹配 :true

注意事項

使用spring boot 2.1.7版本,pom.xml需要引入加解密碼包,如下所示:

        <!-- 加解密包 -->          <dependency>              <groupId>org.bouncycastle</groupId>              <artifactId>bcprov-jdk15on</artifactId>              <version>1.62</version>          </dependency>

否則執行會報錯:java.lang.NoClassDefFoundError: org/bouncycastle/crypto/generators/SCrypt,詳細報錯代碼

Exception in thread "main" java.lang.NoClassDefFoundError: org/bouncycastle/crypto/generators/SCrypt      at org.springframework.security.crypto.scrypt.SCryptPasswordEncoder.digest(SCryptPasswordEncoder.java:167)      at org.springframework.security.crypto.scrypt.SCryptPasswordEncoder.encode(SCryptPasswordEncoder.java:126)      at com.easy.encoder.PasswordEncoderFactoriesTest.main(PasswordEncoderFactoriesTest.java:32)  Caused by: java.lang.ClassNotFoundException: org.bouncycastle.crypto.generators.SCrypt      at java.net.URLClassLoader.findClass(URLClassLoader.java:382)      at java.lang.ClassLoader.loadClass(ClassLoader.java:424)      at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)      at java.lang.ClassLoader.loadClass(ClassLoader.java:357)      ... 3 more

查看報錯代碼,發現SCrypt類找不到引起的異常,引入加解密包解決問題。

資料