数据签名、加密是前后端开发经常需要使用到的技术,应用场景包括不限于用户登入、数据交易、信息通讯等,不同的应用场景也会需要使用到不同的签名加密算法,或者需要搭配不一样的 签名加密算法来达到业务目标。常用的加密算法有:
- 对称加密算法;
- 非对称加密算法;
- 哈希算法,加盐哈希算法(单向加密);
- 数字签名。
使用加密签名算法,可以达到下面的安全目标:
- 保密性:防止用户的数据被读取;
- 数据完整性:防止数据被篡改;
- 身份验证:确保数据发自特定的一方。
对称加密
对称加密算法加密和解密时使用同一把秘钥。操作比较简单,加密速度快,秘钥简单。经常在消息发送方需要加密大量数据时使用。缺点是风险都在这个秘钥上面,一旦被窃取,信息会暴露。所以安全级别不够高。常用对称加密算法有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下面的实现。