為了理解SSL/TLS原理,大家需要掌握一些加密算法的基礎知識。當然,這不是為了讓大家成為密碼學專家,所以只需對基礎的加密算法有一些了解即可。基礎的加密算法主要有哈希(Hash,或稱為散列)?、對稱加密(Symmetric Cryptography)、非對稱加密(Asymmetric Cryptography)、數字簽名(Digital Signature)。
哈希單向加密算法原理與實戰
哈希算法(或稱為散列算法)比較簡單,就是為待加密的任意大小的信息(如字符串)生成一個固定大小(比如通過MD5加密之后是32個字符)的字符串摘要。常用的哈希算法有MD5、SHA1、SHA-512等。哈希是不可逆的加密技術,一些數據一旦通過哈希轉換為其他形式,源數據將永遠無法恢復。
在哪些場景下使用哈希加密呢?一般來說,在用戶注冊的時候,服務端保存用戶密碼的時候會將明文密碼的哈希密碼存儲在數據庫中,而不是直接存儲用戶的明文密碼。當用戶下次登錄時,會對用戶的登入密碼(明文)使用相同的哈希算法進行處理,并將哈希結果與來自數據庫的哈希密碼進行匹配,如果是相同的,那么用戶將登錄成功,否則用戶將登錄失敗。
哈希加密也稱為單向哈希加密,是通過對不同輸入長度的信息進行哈希計算得到固定長度的輸出,是單向、不可逆的。所以,即使保存用戶密碼的數據庫被攻擊,也不會造成用戶的密碼泄漏。
最常見的哈希算法為MD5(Message-Digest Algorithm 5,信息-摘要算法5)?,也是計算機廣泛使用的哈希算法之一。主流編程語言普遍都提供MD5實現,MD5的前身有MD2、MD3和MD4。
MD5將輸入的不定長度信息經過程序流程生成四個32位(Bit)數據,最后聯合起來輸出一個固定長度128位的摘要,基本處理流程包括求余、取余、調整長度、與鏈接變量進行循環運算等,最終得出結果。
除了MD5, Java還提供了SHA1、SHA256、SHA512等哈希摘要函數的實現。除了在算法上有些差異之外,這些哈希函數的主要不同在于摘要長度,MD5生成的摘要是128位,SHA1生成的摘要是160位,SHA256生成的摘要是256位,SHA512生成的摘要是512位。
SHA-1與MD5的最大區別在于其摘要比MD5摘要長32位(相當于長4字節,轉換十六進制后比MD5多8個字符)?。對SHA-1強行攻擊的強度比對MD5攻擊的強度要大。但是SHA-1哈希過程的循環步驟比MD5多,且需要的緩存大,因此SHA-1的運行速度比MD5慢。
以下代碼使用Java提供的MD5、SHA1、SHA256、SHA512等哈希摘要函數生成哈希摘要(哈希加密結果)并進行驗證的案例:
//省略import
public class HashCrypto
{/*** 哈希單向加密測試用例*/public static String encrypt(String plain){StringBuffer md5Str = new StringBuffer(32);try{/*** MD5*///MessageDigest md = MessageDigest.getInstance("MD5");/*** SHA-1*///MessageDigest md = MessageDigest.getInstance("SHA-1");/*** SHA-256*///MessageDigest md = MessageDigest.getInstance("SHA-256");/*** SHA-512*/MessageDigest md = MessageDigest.getInstance("SHA-512");String charset = "UTF-8";byte[] array = md.digest(plain.getBytes(charset));for (int i = 0; i < array.length; i++){//轉成十六進制字符串String hexString = Integer.toHexString((0x000000FF & array[i]) | 0xFFFFFF00);log.debug("hexString: {}, 第6位之后: {}",hexString, hexString.substring(6));md5Str.append(hexString.substring(6));}} catch (Exception ex){ex.printStackTrace();}return md5Str.toString();}public static void main(String[] args){//原始的明文字符串,也是需要加密的對象String plain = "123456";//使用哈希函數加密String cryptoMessage = HashCrypto.encrypt(plain);log.info("cryptoMessage:{}", cryptoMessage);//驗證String cryptoMessage2 = HashCrypto.encrypt(plain);log.info("驗證 {},\n是否一致:{}", cryptoMessage2,cryptoMessage.equals(cryptoMessage2));//驗證2String plainOther = "654321";String cryptoMessage3 = HashCrypto.encrypt(plainOther);log.info("驗證 {},\n是否一致:{}", cryptoMessage3,cryptoMessage.equals(cryptoMessage3));}
}
對稱加密算法原理與實戰
對稱加密(Symmetric Cryptography)指的是客戶端自己封裝一種加密算法,將給服務端發送的數據進行加密,并且將數據加密的方式(密鑰)發送給密文,服務端收到密鑰和數據,用密鑰進行解密。
對稱加密:使用同一個密鑰加密和解密,優點是速度快;但是它要求共享密鑰,缺點是密鑰管理不方便、容易泄露。
常見的對稱加密算法有DES、AES等。DES加密算法出自IBM的數學研究,被美國政府正式采用之后開始廣泛流傳,但是近些年來使用越來越少,因為DES使用56位密鑰,以現代計算能力24小時內即可被破解。雖然如此,但是在對安全要求不高的應用中,還是可以使用DES加密算法。
下面是一段使用Java語言編寫的進行DES加密的演示代碼:
//省略import
public class DESCrypto
{/*** 對稱加密*/public static byte[] encrypt(byte[] data, String password) {try{SecureRandom random = new SecureRandom();//使用密碼,創建一個密鑰描述符DESKeySpec desKey = new DESKeySpec(password.getBytes());//創建一個密鑰工廠,然后用它把 DESKeySpec 密鑰描述符實例轉換成密鑰SecretKeyFactory keyFactory =SecretKeyFactory.getInstance("DES");//通過密鑰工程生成密鑰SecretKey secretKey = keyFactory.generateSecret(desKey);//Cipher對象實際完成加密操作Cipher cipher = Cipher.getInstance("DES");//用密鑰初始化Cipher對象cipher.init(Cipher.ENCRYPT_MODE, secretKey, random);//為數據執行加密操作return cipher.doFinal(data);}catch(Throwable e){e.printStackTrace();}return null;}/*** 對稱解密*/public static byte[] decrypt(byte[] cryptData,String password) …{//DES算法要求有一個可信任的隨機數源SecureRandom random = new SecureRandom();//創建一個 DESKeySpec 密鑰描述符對象DESKeySpec desKey = new DESKeySpec(password.getBytes());//創建一個密鑰工廠SecretKeyFactory keyFactory =SecretKeyFactory.getInstance("DES");//將 DESKeySpec 對象轉換成 SecretKey 對象SecretKey secretKey = keyFactory.generateSecret(desKey);//Cipher對象實際完成解密操作Cipher cipher = Cipher.getInstance("DES");//用密鑰初始化Cipher對象cipher.init(Cipher.DECRYPT_MODE, secretKey, random);//真正開始解密操作return cipher.doFinal(cryptData);}public static void main(String args[]) {//待加密內容String str = "123456";//密碼長度要是8的倍數String password = "12345678";byte[] result = DESCrypto.encrypt(str.getBytes(),password);log.info("str:{} 加密后:{}",str,new String(result));//直接將如上內容解密try {byte[] decryResult = DESCrypto.decrypt(result, password);log.info("解密后:{}",new String(decryResult));} catch (Exception e1) {e1.printStackTrace();}}
}
非對稱加密算法原理與實戰
非對稱加密算法(Asymmetric Cryptography)又稱為公開密鑰加密算法,需要兩個密鑰:一個稱為公開密鑰(公鑰)?;另一個稱為私有密鑰(私鑰)?。公鑰與私鑰需要配對使用,如果用公鑰對數據進行加密,只有用對應的私鑰才能解密;如果使用私鑰對數據加密,那么需要用對應的公鑰才能解密。由于加解密使用不同的密鑰,因此這種算法為非對稱加密算法。
非對稱加密算法的優點是密鑰管理很方便,缺點是速度慢。典型的非對稱加密算法有RSA、DSA等。
下面是一段使用Java代碼進行RSA加密的演示代碼:
//省略import
/*** RSA 非對稱加密算法*/
@Slf4j
public class RSAEncrypt
{/*** 指定加密算法為RSA*/private static final String ALGORITHM = "RSA";/*** 常量,用來初始化密鑰長度*/private static final int KEY_SIZE = 1024;/*** 指定公鑰存放文件*/private static final String PUBLIC_KEY_FILE =SystemConfig.getKeystoreDir() + "/PublicKey";/*** 指定私鑰存放文件*/private static final String PRIVATE_KEY_FILE =SystemConfig.getKeystoreDir() + "/PrivateKey";/*** 生成密鑰對*/protected static void generateKeyPair() throws Exception{/*** 為RSA算法創建一個KeyPairGenerator對象*/KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance(ALGORITHM);/*** 利用上面的密鑰長度初始化這個KeyPairGenerator對象*/keyPairGenerator.initialize(KEY_SIZE);/** 生成密鑰對 */KeyPair keyPair = keyPairGenerator.generateKeyPair();/** 得到公鑰 */PublicKey publicKey = keyPair.getPublic();/** 得到私鑰 */PrivateKey privateKey = keyPair.getPrivate();ObjectOutputStream oos1 = null;ObjectOutputStream oos2 = null;try{log.info("生成公鑰和私鑰,并且寫入對應的文件");File file = new File(PUBLIC_KEY_FILE);if (file.exists()){log.info("公鑰和私鑰已經生成,不需要重復生成,path:{}", PUBLIC_KEY_FILE);return;}/** 用對象流將生成的密鑰寫入文件 */log.info("PUBLIC_KEY_FILE 寫入:{}", PUBLIC_KEY_FILE);oos1 = new ObjectOutputStream(new FileOutputStream(PUBLIC_KEY_FILE));log.info("PRIVATE_KEY_FILE 寫入:{}", PRIVATE_KEY_FILE);oos2 = new ObjectOutputStream(new FileOutputStream(PRIVATE_KEY_FILE));oos1.writeObject(publicKey);oos2.writeObject(privateKey);} catch (Exception e){throw e;} finally{/** 清空緩存,關閉文件輸出流 */IOUtil.closeQuietly(oos1);IOUtil.closeQuietly(oos2);}}/*** 加密方法,使用公鑰加密* @param plain 明文數據*/public static String encrypt(String plain) throws Exception{//從文件加載公鑰Key publicKey = loadPublicKey();/** 得到Cipher對象,來實現對源數據的RSA加密 */Cipher cipher = Cipher.getInstance(ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, publicKey);byte[] b = plain.getBytes();/** 執行加密操作 */byte[] b1 = cipher.doFinal(b);BASE64Encoder encoder = new BASE64Encoder();return encoder.encode(b1);}/*** 從文件加載公鑰*/public static PublicKey loadPublicKey() throws Exception{PublicKey publicKey=null;ObjectInputStream ois = null;try{log.info("PUBLIC_KEY_FILE 讀取:{}", PUBLIC_KEY_FILE);/** 讀出文件中的公鑰 */ois = new ObjectInputStream(new FileInputStream(PUBLIC_KEY_FILE));publicKey = (PublicKey) ois.readObject();} catch (Exception e){throw e;} finally{IOUtil.closeQuietly(ois);}return publicKey;}//方法:對密文解密,使用私鑰解密public static String decrypt(String crypto) throws Exception{PrivateKey privateKey = loadPrivateKey();/** 得到Cipher對象,對已用公鑰加密的數據進行RSA解密 */Cipher cipher = Cipher.getInstance(ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, privateKey);BASE64Decoder decoder = new BASE64Decoder();byte[] b1 = decoder.decodeBuffer(crypto);/** 執行解密操作 */byte[] b = cipher.doFinal(b1);return new String(b);}/*** 從文件加載私鑰* @throws Exception*/public static PrivateKey loadPrivateKey() throws Exception{PrivateKey privateKey;ObjectInputStream ois = null;try{log.info("PRIVATE_KEY_FILE 讀取:{}", PRIVATE_KEY_FILE);/** 讀出文件中的私鑰 */ois = new ObjectInputStream(new FileInputStream(PRIVATE_KEY_FILE));privateKey = (PrivateKey) ois.readObject();} catch (Exception e){e.printStackTrace();throw e;} finally{IOUtil.closeQuietly(ois);}return privateKey;}public static void main(String[] args) throws Exception{//生成密鑰對generateKeyPair();//待加密內容String plain = "123";//公鑰加密String dest = encrypt(plain);log.info("{} 使用公鑰加密后:\n{}", plain, dest);//私鑰解密String decrypted = decrypt(dest);log.info(" 使用私鑰解密后:\n{}", decrypted);}
}
非對稱加密算法包含兩種密鑰,其中的公鑰本來是公開的,不需要像對稱加密算法那樣將私鑰給對方,對方解密時使用公開的公鑰即可,大大地提高了加密算法的安全性。退一步講,即使不法之徒獲知了非對稱加密算法的公鑰,甚至獲知了加密算法的源碼,只要沒有獲取公鑰對應的私鑰,也是無法進行解密的。
數字簽名原理與實戰
數字簽名(Digital Signature)是確定消息發送方身份的一種方案。在非對稱加密算法中,發送方A通過接收方B的公鑰將數據加密后的密文發送給接收方B, B利用私鑰解密就得到了需要的數據。這里還存在一個問題,接收方B的公鑰是公開的,接收方B收到的密文都是使用自己的公鑰加密的,那么如何檢驗發送方A的身份呢?
一種非常簡單的檢驗發送方A身份的方法為:發送方A可以利用A自己的私鑰進行消息加密,然后B利用A的公鑰來解密,由于私鑰只有A知道,接收方只要解密成功,就可以確定消息來自A而不是其他地方。
數字簽名的原理就基于此,通常為了證明發送數據的真實性,利用發送方的私鑰對待發送的數據生成數字簽名。
數字簽名的流程比較簡單,首先通過哈希函數為待發數據生成較短的消息摘要,然后利用私鑰加密該摘要,所得到的摘要密文基本上就是數字簽名。發送方A將待發送數據以及數字簽名一起發送給接收方B,接收方B收到之后使用A的公鑰校驗數字簽名,如果校驗成功,就說明內容來自發送方A,否則為非法內容。
數字簽名的大致流程如圖12-7所示。
Java為數字簽名提供了良好的支持,java.security.Signature接口提供了數字簽名的基本操作API, Java規范要求各JDK版本需要提供表12-2中所列出的標準簽名實現。
下面是一段使用JSHA512withRSA算法實現數字簽名的Java演示代碼:
package com.crazymakercircle.secure.crypto;
//省略import
/*** RSA簽名演示*/
@Slf4j
public class RSASignDemo
{/*** RSA簽名** @param data 待簽名的字符串* @param priKey RSA私鑰字符串* @return 簽名結果* @throws Exception 簽名失敗則拋出異常*/public byte[] rsaSign(byte[] data, PrivateKey priKey)throws SignatureException{try{Signature signature = Signature.getInstance("SHA512withRSA");signature.initSign(priKey);signature.update(data);byte[] signed = signature.sign();return signed;} catch (Exception e){throw new SignatureException("RSAcontent = " + data+ "; charset = ", e);}}/*** RSA驗簽* @param data 被簽名的內容* @param sign 簽名后的結果* @param pubKey RSA公鑰* @return 驗簽結果*/public boolean verify(byte[] data, byte[] sign, PublicKey pubKey)throws SignatureException{try{Signature signature = Signature.getInstance("SHA512withRSA");signature.initVerify(pubKey);signature.update(data);return signature.verify(sign);} catch (Exception e){e.printStackTrace();throw new SignatureException("RSA驗證簽名[content = " + data+"; charset = " + "; signature = " + sign + "]發生異常!", e);}}/*** 私鑰*/private PrivateKey privateKey;/*** 公鑰*/private PublicKey publicKey;/*** 加密過程* @param publicKey 公鑰* @param plainTextData 明文數據* @throws Exception 加密過程中的異常信息*/public byte[] encrypt(PublicKey publicKey, byte[] plainTextData)throws Exception{if (publicKey == null){throw new Exception("加密公鑰為空, 請設置");}Cipher cipher = null;try{cipher = Cipher.getInstance("RSA");cipher.init(Cipher.ENCRYPT_MODE, publicKey);byte[] output = cipher.doFinal(plainTextData);return output;} catch (NoSuchAlgorithmException e){throw new Exception("無此加密算法");}…}/*** 解密過程* @param privateKey 私鑰* @param cipherData 密文數據* @return 明文* @throws Exception 解密過程中的異常信息*/public byte[] decrypt(PrivateKey privateKey, byte[] cipherData)…{if (privateKey == null){throw new Exception("解密私鑰為空, 請設置");}Cipher cipher = null;try{cipher = Cipher.getInstance("RSA");cipher.init(Cipher.DECRYPT_MODE, privateKey);byte[] output = cipher.doFinal(cipherData);return output;} catch (NoSuchAlgorithmException e){throw new Exception("無此解密算法");}…}/*** Main 測試方法* @param args*/public static void main(String[] args) throws Exception{RSASignDemo RSASignDemo = new RSASignDemo();//加載公鑰RSASignDemo.publicKey = RSAEncrypt.loadPublicKey();//加載私鑰RSASignDemo.privateKey = RSAEncrypt.loadPrivateKey();//測試字符串String sourceText = "12312";try{log.info("加密前的字符串為:{}", sourceText);//公鑰加密byte[] cipher = RSASignDemo.encrypt(RSASignDemo.publicKey, sourceText.getBytes());//私鑰解密byte[] decryptText = RSASignDemo.decrypt(RSASignDemo.privateKey, cipher);log.info("私鑰解密的結果是:{}", new String(decryptText));//字符串生成簽名byte[] rsaSign = RSASignDemo.rsaSign(sourceText.getBytes(), RSASignDemo.privateKey);//簽名驗證Boolean succeed = RSASignDemo.verify(sourceText.getBytes(),rsaSign, RSASignDemo.publicKey);log.info("字符串簽名為:\n{}", byteToHex(rsaSign));log.info("簽名驗證結果是:{}", succeed);String fileName =IOUtil.getResourcePath("/system.properties");byte[] fileBytes = readFileByBytes(fileName);//文件簽名驗證byte[] fileSign =RSASignDemo.rsaSign(fileBytes, RSASignDemo.privateKey);log.info("文件簽名為:\n{}" , byteToHex(fileSign));//文件簽名保存String signPath =SystemConfig.getKeystoreDir() + "/fileSign.sign";ByteUtil.saveFile(fileSign,signPath );Boolean verifyOK = RSASignDemo.verify(fileBytes, fileSign, RSASignDemo.publicKey);log.info("文件簽名驗證結果是:{}", verifyOK);//讀取驗證文件byte[] read = readFileByBytes(signPath);log.info("讀取文件簽名:\n{}" , byteToHex(read));verifyOK= RSASignDemo.verify(fileBytes, read, RSASignDemo.publicKey);log.info("讀取文件簽名驗證結果是:{}", verifyOK);} catch (Exception e){System.err.println(e.getMessage());}}
}