缘起

网络中为了保障数据安全性,会对数据做很多增加安全性的操作,比如加密,签名(摘要)等。而加密又分为对称加密和非对称加密,每一种加密方式又有不同加密算法来实现,常见的有比如MD5,DES,AES等,本文主要对常用的加密算法做一个入门。

加密算法分类

Java加密与解密艺术_base64

单向散列算法

单向散列算法又叫签名摘要算法或摘要算法,是从一段明文中提取一段摘要出来,明文中任何一个字符的变动都会引起摘要的变动,从而实现对原文的真实性做校验
比如有一个压缩包A求出来的摘要为c0bb4f54f1d8b14caf6fe1069e5f93ad,那么用户在下载这个压缩包到本地的时候就可以再次求一下摘要,然后与作者提供的摘要做比对,如果不一样,说明这个压缩包被人修改过,可能被人放入了病毒之类。
常见的单向散列算法有MD系列,Sha系列,Mac算法。
MD5:md5算法是典型的消息摘要算法,其前身有MD2、MD3和MD4算法,它由MD4、MD3和MD2算法改进而来。

SHA系列:也是由MD4演化而来,包括 SHA-1,SHA-224,SHA-256,SHA-384,和 SHA-512 等几种算法。其中,SHA-1,SHA-224 和 SHA-256 适用于长度不超过 2^64 二进制位的消息。SHA-384 和 SHA-512 适用于长度不超过 2^128 二进制位的消息。与MD5主要不同之处在于摘要长度,SHA算法的摘要长度更长,安全性更高。

HMAC:类似MD5和SHA,也是给定一个明文,通过HMAC得到这段明文的Hash值,但是与MD和SHA不同的是,HMAC运算的时候需要一个密钥的参与,通常称之为“盐”,其原理为 HMAC 进程将密钥(盐)与消息数据混合,使用哈希函数对混合结果进行哈希计算,将所得哈希值与该密钥混合,然后再次应用哈希函数。 输出的哈希值长度为 160 位。此外,HMAC还能自己生成密钥而非人为构造密钥。调用的时候通常是hamc("明文","盐")这种方式。MD5和SHA系列在日常的使用过程中为了安全通常也会加盐,只是这个盐是直接拼在明文的后面,比如:MD5("明文"+"盐").HMAC可以用来做报文签名,也就是防篡改,发送方在发送报文的时候在报文后面拼接上通过HMAC算法计算出的Hash值,接收方接收到的时候对这个Hash值进行验证,由于密钥(盐)是发送方和接收方提前约定好的,所以中间层即使获取了你的明文,他篡改了报文之后由于没有盐,他就无法得到运算之后的结果。这样接收端方可以验证这段报文的真实性。Java实现Hamc如下

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class HMACSHA1 {

    private static final String MAC_NAME = "HmacSHA1";
    private static final String ENCODING = "UTF-8";

    /*
     * 展示了一个生成指定算法密钥的过程 初始化HMAC密钥
     * @return
     * @throws Exception
     *
      public static String initMacKey() throws Exception {
      //得到一个 指定算法密钥的密钥生成器
      KeyGenerator KeyGenerator keyGenerator =KeyGenerator.getInstance(MAC_NAME);
      //生成一个密钥
      SecretKey secretKey =keyGenerator.generateKey();
      return null;
      }
     */

    /**
     * 使用 HMAC-SHA1 签名方法对对encryptText进行签名
     * @param encryptText 被签名的字符串
     * @param encryptKey  密钥
     * @return
     * @throws Exception
     */
    public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception
    {
        byte[] data=encryptKey.getBytes(ENCODING);
        //根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称
        SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
        //生成一个指定 Mac 算法 的 Mac 对象
        Mac mac = Mac.getInstance(MAC_NAME);
        //用给定密钥初始化 Mac 对象
        mac.init(secretKey);

        byte[] text = encryptText.getBytes(ENCODING);
        //完成 Mac 操作
        return mac.doFinal(text);
    }
}
对称加密算法

DES:使用最广泛的算法,DES 的密钥的位数太短,只有56 比特,加密强度不够,而如果要提高加密强度(例如增加密钥长度),则系统开销呈指数增长。

3DES:解决DES密钥太短的问题,密钥长度112-168位,增加了迭代次数,速度较慢,效率不高。使用方式同DES。

AES:解决3DES加密效率低,速度慢的问题,成为DES算法的替代者,使用方式同DES。AES是下一代的加密算法标准,速度快,安全级别高。对内存的需求非常低。AES的密钥长度比DES大, AES同时还有AES128,AES192,AES256这些,后面的数字分别代表密钥的比特长度。AES所允许的密钥长度有128(bit),192(bit),256(bit)三个长度,对应的明文就是128/8=16字节,24字节和32字节。但是由于美国出口限制,导致目前只能用16个字节的密钥,用24和32字节的密钥会报错java.security.InvalidKeyException: Illegal key size or default parameters

不管是DES,3DES还是AES的又有五种加密模式:CBC、ECB、CTR、OCF、CFB,每一种加密模式其实现机理又不一样,加密出来的密文也不尽相同。

非对称加密算法

RSA:该算法有一个公钥和一个私钥。用公钥加密只有这个私钥能解开,用私钥加密也只有公钥能解开。在日常使用中通常私钥是只有服务端有,而公钥是多个客户端有。这样服务端发送的报文每一个客户端都可以解开(此处的应用场景通常用于签名,因为私钥只有服务端有,所以客户端能够解密出来这一段密文,就说明报文是服务端发送的而不是第三方发送的)。而客户端发送的报文也只有服务端能够解开,客户端互相之间是解不开的。故日常用私钥签名,公钥加密。

编码算法

严格来说编码算法并不属于加密算法,其原理只是对原文进行编码使之变成了一串肉眼无法辨别的编码,但如果通过计算机,能够很容易的再解码为明文,没有任何安全性可言。

BASE64:由于某些系统中只能使用ASCII字符。Base64就是用来将非ASCII字符的数据转换成ASCII字符的一种方法。由于ASCII码表是不包含中文的,所以Base64理论上应该不能作用于中文,但实际测试过程中确实是可以,这是什么原因我也不是很清楚,猜测只是把中文转成了字符而已。

URIEncode:由于url中对一些字符有限制,比如“+”,“/”等,而传统base64编码后会出现这些字符,这些字符会影响URL的使用,因此出现了UrlBase64编码。

JDK中对加密算法的支持

jdk中对加密算法有不错的支持,我们来看看具体有哪些。
jdk加密相关jar包以及package:

  1. rt.jar:java.security.*包,定义了消息摘要算法的实现(不包括mac算法)以及加解密算法的使用接口
  2. jce.jar:java.crypto.*包,DES,3DES以及AES中使用较多,RSA中会用到Cipher类。其中包含了DES,3DES以及AES的实现;比较常用的类Cipher,KeyGenerator,SecretKey,Key,SecretKeyFactory
  3. rt.jar:java.security.cert.*包,提供用于解析和管理证书的类和接口,如Certificate以及CertificateFactory
  4. Bouncy Castle:加密组件,安全提供者,javajce的补充(如java6不支持MD4,SSH-224,但bc支持),以及一些工具类,如Base64编码(UrlBase64类)以及十六进制编码(Hex类)。

此外还有很多开源组织也提供了很多不错的工具类,比如apache的commons-codec,apache的commons是一个工具包,而commons-codec就是这个工具包中用于加解密的。

下面给出一个在java中使用各种加密方式的代码

package com.bxoon.security;

import org.junit.jupiter.api.Test;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.*;

/**
 * MD5加密
 */
public class MD5Util {

    /**
     * 功能简述: 测试MD5单向加密.
     * @throws Exception
     */
    @Test
    public void test01() throws Exception {
        String plainText = "Hello , world !";
        MessageDigest md5 = MessageDigest.getInstance("md5");
        byte[] cipherData = md5.digest(plainText.getBytes());
        StringBuilder builder = new StringBuilder();
        for(byte cipher : cipherData) {
            String toHexStr = Integer.toHexString(cipher & 0xff);
            builder.append(toHexStr.length() == 1 ? "0" + toHexStr : toHexStr);
        }
        System.out.println(builder.toString());
    }

    /**
     * 功能简述: 使用BASE64进行双向加密/解密.
     * @throws Exception
     */
    @Test
    public void test02() throws Exception {
        BASE64Encoder encoder = new BASE64Encoder();
        BASE64Decoder decoder = new BASE64Decoder();
        String plainText = "Hello , world !";
        String cipherText = encoder.encode(plainText.getBytes());
        System.out.println("cipherText : " + cipherText);
        //cipherText : SGVsbG8gLCB3b3JsZCAh
        System.out.println("plainText : " +
                new String(decoder.decodeBuffer(cipherText)));
    }

    /**
     * 功能简述: 使用DES对称加密/解密.
     * @throws Exception
     */
    @Test
    public void test03() throws Exception {
        String plainText = "Hello , world !";
        String key = "12345678";    //要求key至少长度为8个字符

        SecureRandom random = new SecureRandom();
        DESKeySpec keySpec = new DESKeySpec(key.getBytes());
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("des");
        SecretKey secretKey = keyFactory.generateSecret(keySpec);

        Cipher cipher = Cipher.getInstance("des");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, random);
        byte[] cipherData = cipher.doFinal(plainText.getBytes());
        System.out.println("cipherText : " + new BASE64Encoder().encode(cipherData));
        //PtRYi3sp7TOR69UrKEIicA==

        cipher.init(Cipher.DECRYPT_MODE, secretKey, random);
        byte[] plainData = cipher.doFinal(cipherData);
        System.out.println("plainText : " + new String(plainData));
        //Hello , world !
    }

    /**
     * 功能简述: 使用RSA非对称加密/解密.
     * @throws Exception
     */
    @Test
    public void test04() throws Exception {
        String plainText = "Hello , world !";

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("rsa");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();
        SecureRandom random = new SecureRandom();

        Cipher cipher = Cipher.getInstance("rsa");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey, random);
        byte[] cipherData = cipher.doFinal(plainText.getBytes());
        System.out.println("cipherText : " + new BASE64Encoder().encode(cipherData));

        cipher.init(Cipher.DECRYPT_MODE, publicKey, random);
        byte[] plainData = cipher.doFinal(cipherData);
        System.out.println("plainText : " + new String(plainData));
        //Hello , world !

        Signature signature  = Signature.getInstance("MD5withRSA");
        signature.initSign(privateKey);
        signature.update(cipherData);
        byte[] signData = signature.sign();
        System.out.println("signature : " + new BASE64Encoder().encode(signData));
        //ADfoeKQn6eEHgLF8ETMXan3TfFO03R5u+cQEWtAQ2lRblLZw1DpzTlJJt1RXjU451I84v3297LhR
        //co64p6Sq3kVt84wnRsQw5mucZnY+jRZNdXpcbwh2qsh8287NM2hxWqp4OOCf/+vKKXZ3pbJMNT/4
        ///t9ewo+KYCWKOgvu5QQ=

        signature.initVerify(publicKey);
        signature.update(cipherData);
        boolean status = signature.verify(signData);
        System.out.println("status : " + status);
        //true
    }

}