數據簽名、加密是前后端開發經常需要使用到的技術,應用場景包括不限於用戶登入、數據交易、信息通訊等,不同的應用場景也會需要使用到不同的簽名加密算法,或者需要搭配不一樣的 簽名加密算法來達到業務目標。常用的加密算法有:
- 對稱加密算法;
- 非對稱加密算法;
- 哈希算法,加鹽哈希算法(單向加密);
- 數字簽名。
使用加密簽名算法,可以達到下面的安全目標:
- 保密性:防止用戶的數據被讀取;
- 數據完整性:防止數據被篡改;
- 身份驗證:確保數據發自特定的一方。
對稱加密
對稱加密算法加密和解密時使用同一把秘鑰。操作比較簡單,加密速度快,秘鑰簡單。經常在消息發送方需要加密大量數據時使用。缺點是風險都在這個秘鑰上面,一旦被竊取,信息會暴露。所以安全級別不夠高。常用對稱加密算法有DES,3DES,AES等。在jdk中也都有封裝。
DES
DES的秘鑰為8個字節,64個bit位。(不適應當今分布式開放網絡對數據加密安全性的要求)在Java進行DES、3DES和AES三種對稱加密算法時,常采用的是NoPadding(不填充)、Zeros填充(0填充)、PKCS5Padding填充。
一個DES的列子:
3DES
3DES(或稱為Triple DES)是三重數據加密算法(TDEA,Triple Data Encryption Algorithm)塊密碼的通稱。它相當於是對每個數據塊應用三次DES加密算法。由於計算機運算能力的增強,原版DES密碼的密鑰長度變得容易被暴力破解;3DES即是設計用來提供一種相對簡單的方法,即通過增加DES的密鑰長度來避免類似的攻擊,而不是設計一種全新的塊密碼算法。
其具體實現如下:設Ek()和Dk()代表DES算法的加密和解密過程,K代表DES算法使用的密鑰,P代表明文,C代表密文,這樣:
3DES加密過程為:C=Ek3(Dk2(Ek1(P)))
3DES解密過程為:P=Dk1(EK2(Dk3(C)))
AES
高級加密標准(英語:Advanced Encryption Standard,縮寫:AES),是一種區塊加密標准。這個標准用來替代原先的DES,已經被多方分析且廣為全世界所使用。
那么為什么原來的DES會被取代呢?原因就在於其使用56位密鑰,比較容易被破解。而AES可以使用128bit位、192、和256位密鑰,並且用128位分組加密和解密數據,相對來說安全很多。完善的加密算法在理論上是無法破解的,除非使用窮盡法。
非對稱加密
非對稱加密,顧名思義就是加密與解密的過程不是對稱的,不是用的同一個秘鑰。非對稱加密有個公私鑰對的概念,也就是有兩把秘鑰,一把是公鑰,一把是私鑰,一對公私鑰有固定的生成方法,在加密的時候,用公鑰去加密,接收方再用對應的私鑰去解密。使用時可以由接收方生成公私鑰對,然后將公鑰傳給加密方,這樣私鑰不會在網絡中傳輸,沒有被竊取的風險。比如github底層的ssh協議就是公私鑰非對稱加密。並且公鑰是可以由私鑰推導出來的,反過來卻不行,由通過公鑰無法推導出私鑰。常用算法有RSA,DSA,ECC等。ECC也是比特幣底層用的比較多的算法。通過和對稱加密的對比,可以看到,非對稱加密解決了秘鑰傳輸中的安全問題。
RSA加密算法
RSA
加密算法是目前最有影響力的公鑰加密算法,並且被普遍認為是目前最優秀的公鑰方案 之一。RSA 是第一個能同時用於加密和數字簽名的算法,它能夠抵抗到目前為止已知的所有密碼攻擊,已被ISO推薦為公鑰數據加密標准。
相同長度的秘鑰,RSA和DSA的安全性差不多。一般情況下DSA多用於數字簽名,簽名的效率比RSA更高。RSA支持加密和加簽操作。所以當我們需要同時進行加密和加簽操作的時候一般選擇RSA算法。
這邊提供一個在線生成RSA公私鑰對的網站,可以選擇生成512,1024,2048或者是4096位的秘鑰。使用起來比較方便。
下面給出一個RSA算法的列子:
DSA
一般用於數字簽名場合。
ECC
ECC 也是一種 非對稱加密算法,主要優勢是在某些情況下,它比其他的方法使用 更小的密鑰,比如 RSA 加密算法,提供 相當的或更高等級 的安全級別。不過一個缺點是 加密和解密操作 的實現比其他機制 時間長 (相比 RSA 算法,該算法對 CPU 消耗嚴重)。
import net.pocrd.annotation.NotThreadSafe; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.Cipher; import java.io.ByteArrayOutputStream; import java.security.KeyFactory; import java.security.Security; import java.security.Signature; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; @NotThreadSafe public class EccHelper { private static final Logger logger = LoggerFactory.getLogger(EccHelper.class); private static final int SIZE = 4096; private BCECPublicKey publicKey; private BCECPrivateKey privateKey; static { Security.addProvider(new BouncyCastleProvider()); } public EccHelper(String publicKey, String privateKey) { this(Base64Util.decode(publicKey), Base64Util.decode(privateKey)); } public EccHelper(byte[] publicKey, byte[] privateKey) { try { KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); if (publicKey != null && publicKey.length > 0) { this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey)); } if (privateKey != null && privateKey.length > 0) { this.privateKey = (BCECPrivateKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey)); } } catch (ClassCastException e) { throw new RuntimeException("", e); } catch (Exception e) { throw new RuntimeException(e); } } public EccHelper(String publicKey) { this(Base64Util.decode(publicKey)); } public EccHelper(byte[] publicKey) { try { KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); if (publicKey != null && publicKey.length > 0) { this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey)); } } catch (Exception e) { throw new RuntimeException(e); } } public byte[] encrypt(byte[] content) { if (publicKey == null) { throw new RuntimeException("public key is null."); } try { Cipher cipher = Cipher.getInstance("ECIES", "BC"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); int size = SIZE; ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 45)); int left = 0; for (int i = 0; i < content.length; ) { left = content.length - i; if (left > size) { cipher.update(content, i, size); i += size; } else { cipher.update(content, i, left); i += left; } baos.write(cipher.doFinal()); } return baos.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); } } public byte[] decrypt(byte[] secret) { if (privateKey == null) { throw new RuntimeException("private key is null."); } try { Cipher cipher = Cipher.getInstance("ECIES", "BC"); cipher.init(Cipher.DECRYPT_MODE, privateKey); int size = SIZE + 45; ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size + 44) / (size + 45) * size); int left = 0; for (int i = 0; i < secret.length; ) { left = secret.length - i; if (left > size) { cipher.update(secret, i, size); i += size; } else { cipher.update(secret, i, left); i += left; } baos.write(cipher.doFinal()); } return baos.toByteArray(); } catch (Exception e) { logger.error("ecc decrypt failed.", e); } return null; } public byte[] sign(byte[] content) { if (privateKey == null) { throw new RuntimeException("private key is null."); } try { Signature signature = Signature.getInstance("SHA1withECDSA", "BC"); signature.initSign(privateKey); signature.update(content); return signature.sign(); } catch (Exception e) { throw new RuntimeException(e); } } public boolean verify(byte[] sign, byte[] content) { if (publicKey == null) { throw new RuntimeException("public key is null."); } try { Signature signature = Signature.getInstance("SHA1withECDSA", "BC"); signature.initVerify(publicKey); signature.update(content); return signature.verify(sign); } catch (Exception e) { logger.error("ecc verify failed.", e); } return false; } }
哈希算法(單向加密)
單向加密算法只能用於對數據的加密,無法被解密,其特點為定長輸出、雪崩效應。單向加密算法用於不需要對信息進行解密或讀取的場合,比如用來比較兩個信息值是否一樣而不需要知道信息具體內容,在實際中的一個典型應用就是對數據庫中的用戶信息進行加密,比如當創建一個新用戶及密碼時,將這些信息經過單向加密后再保存到數據庫中。
常見的算法包括
- MD5;
- SHA等
MD5
MD5即Message-Digest Algorithm 5(信息-摘要算法5),用於確保信息傳輸完整一致。常用於數據庫密碼存儲。MD5值是128bit位的數據,一般情況下使用一個長度是32的十六進制字符串來顯示。 具體特點如下:
-
壓縮性:任意長度的數據,算出的MD5值長度都是固定的。
-
容易計算:從原數據計算出MD5值很容易。
-
抗修改性:對原數據進行任何改動,哪怕只修改1個字節,所得到的MD5值都有很大區別。
-
強抗碰撞:已知原數據和其MD5值,想找到一個具有相同MD5值的數據(即偽造數據)是非常困難的。
MD5加鹽
我們知道,如果直接對密碼進行散列,那么黑客可以對通過獲得這個密碼散列值,然后通過查散列值字典(例如MD5密碼破解網站),得到某用戶的密碼。加Salt可以一定程度上解決這一問題。所謂加Salt方法,就是加點“佐料”。其基本想法是這樣的:當用戶首次提供密碼時(通常是注冊時), 由系統自動往這個密碼里撒一些“佐料”,然后再散列。而當用戶登錄時,系統為用戶提供的代碼撒上同樣的“佐料”,然后散列,再比較散列值,已確定密碼是否 正確。
這里的“佐料”被稱作“Salt值”,這個值是由系統隨機生成的,並且只有系統知道。這樣,即便兩個用戶使用了同一個密碼,由於系統為它們生成 的salt值不同,他們的散列值也是不同的。即便黑客可以通過自己的密碼和自己生成的散列值來找具有特定密碼的用戶,但這個幾率太小了(密碼和salt值 都得和黑客使用的一樣才行)。
import java.util.Random; import org.apache.commons.codec.binary.Hex; import java.security.NoSuchAlgorithmException; import java.security.MessageDigest; public class MD5Util { /** * 普通MD5方法 容易被破解 */ public static String md5(String input) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { return "check jdk"; } catch (Exception e) { e.printStackTrace(); return ""; } char[] charArray = input.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) { byteArray[i] = (byte) charArray[i]; } byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) { hexValue.append("0"); } hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } /** * 加鹽MD5 * @author daniel * @time 2016-6-11 下午8:45:04 * @param password * @return */ public static String md5WithSalt(String password) { Random r = new Random(); StringBuilder sb = new StringBuilder(16); sb.append(r.nextInt(99999999)).append(r.nextInt(99999999)); int len = sb.length(); if (len < 16) { for (int i = 0; i < 16 - len; i++) { sb.append("0"); } } String salt = sb.toString(); password = md5Hex(password + salt); char[] cs = new char[48]; for (int i = 0; i < 48; i += 3) { cs[i] = password.charAt(i / 3 * 2); char c = salt.charAt(i / 3); cs[i + 1] = c; cs[i + 2] = password.charAt(i / 3 * 2 + 1); } return new String(cs); } /** * 校驗加鹽后是否和原文一致 * @author daniel * @time 2016-6-11 下午8:45:39 * @param password * @param md5 * @return */ public static boolean verify(String password, String md5) { char[] cs1 = new char[32]; char[] cs2 = new char[16]; for (int i = 0; i < 48; i += 3) { cs1[i / 3 * 2] = md5.charAt(i); cs1[i / 3 * 2 + 1] = md5.charAt(i + 2); cs2[i / 3] = md5.charAt(i + 1); } String salt = new String(cs2); return md5Hex(password + salt).equals(new String(cs1)); } /** * 獲取十六進制字符串形式的MD5摘要 */ private static String md5Hex(String src) { try { MessageDigest md5 = MessageDigest.getInstance("md5"); byte[] bs = md5.digest(src.getBytes()); return new String(new Hex().encode(bs)); } catch (Exception e) { return null; } } public static void main(String[] args) { String md5 = md5("admin"); System.out.println(md5); String mdsSalt = md5WithSalt("admin"); System.out.println(mdsSalt); System.out.println(verify("admin",mdsSalt)); } }
SHA#
SHA代表安全散列算法,SHA-1和SHA-2是該算法的兩個不同版本。它們在構造(如何從原始數據創建結果散列)和簽名的位長方面都不同。您應該將SHA-2視為SHA-1的繼承者,因為它是一個整體改進。
首先,人們把重點放在比特長度上作為重要的區別。SHA-1是160位散列。SHA-2實際上是哈希的“家族”,有各種長度,最受歡迎的是256位。
各種各樣的SHA-2哈希可能會引起一些混亂,因為網站和作者以不同的方式表達它們。如果你看到“SHA-2”,“SHA-256”或“SHA-256位”,那些名稱指的是同一個東西。如果您看到“SHA-224”,“SHA-384”或“SHA-512”,則它們指的是SHA-2的備用位長度。您可能還會看到一些網站更明確,並寫出算法和比特長度,例如“SHA-2 384”。
各個加密算法的比較#
- 散列算法的比較
名稱 | 安全性 | 速度 |
---|---|---|
SHA-1 | 高 | 慢 |
MD5 | 中 | 快 |
- 對稱加密算法比較
名稱 | 密鑰名稱 | 運行速度 | 安全性 | 資源消耗 |
---|---|---|---|---|
DES | 56位 | 較快 | 低 | 中 |
3DES | 112位或168位 | 慢 | 中 | 高 |
AES | 128、192、256位 | 快 | 高 | 低 |
- 非對稱算法的比較
名稱 | 成熟度 | 安全性 | 運算速度 | 資源消耗 |
---|---|---|---|---|
RSA | 高 | 高 | 中 | 中 |
ECC | 高 | 高 | 慢 | 高 |
Base64(編碼方式)
我們知道在計算機中任何數據都是按ascii碼存儲的,而ascii碼的128~255之間的值是不可見字符。而在網絡上交換數據時,比如說從A地傳到B地,往往要經過多個路由設備,由於不同的設備對字符的處理方式有一些不同,這樣那些不可見字符就有可能被處理錯誤,這是不利於傳輸的。所以就先把數據先做一個Base64編碼,統統變成可見字符,這樣出錯的可能性就大降低了。
對證書來說,特別是根證書,一般都是作Base64編碼的,因為它要在網上被許多人下載。電子郵件的附件一般也作Base64編碼的,因為一個附件數據往往是有不可見字符的。標准base64只有64個字符(英文大小寫、數字和+、/)以及用作后綴等號;
Base64有很多實現,JDK默認實現、Apache包下面的實現和Spring提供的實現等。平時我們用的時候推薦使用Apache下面的實現。