为了理解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());}}
}