js与java对接AES-128-GCM加密、解密算法

一、什么是AES加密

常见的加密主要分为两类:对称加密非对称加密,AES加密就是对称加密的一种,即加密和解密使用相同的一把密钥。它的全称是Advanced Encryption Standard(高级加密标准),主要是用来取代DES加密算法,目前已经被全世界广泛采用。

二、AES的基本构成

 

概念:

  • 明文P(plainText):未经加密的数据
  • 密钥K(key):用来加密明文的密码。在对称加密算法中,加密与解密的密钥是相同的,由双方协商产生,绝不可以泄漏
  • 密文C(cipherText): 经过加密的数据
  • 加密函数E(encrypt):C = E(K, P),即将明文和密钥作为参数,传入加密函数中,就可以获得密文
  • 解密函数D(decrypt):P = D(K, C),即将密文和密钥作为参数,传入解密函数中,就可以获得明文

AES的构成:

  • 分组(或者叫块):ES是一种分组加密技术,分组加密就是把明文分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文。那你可能要问:为何要进行分组呢?比如一个应用程序总共就只能获得3M的内存空间来执行,而需要加密的文件是100M,这个时候就不得不进行文件拆解加密。在AES标准规范中,分组长度只能是128 bits,也就是每个分组为16个bytes
  • 密钥长度:AES支持的密钥长度可以是128 bits、192 bits或256 bits。密钥的长度不同,推荐加密轮数也不同

    

 

    加密轮数越多,当然安全性越好,但也更耗费时间

  • 加密模式:因为分组加密只能加密固定长度的分组,而实际需要加密的明文可能超过分组长度,此时就要对分组密码算法进行迭代,以完成整个明文加密,迭代的方法就是加密模式。它有很多种,常见的工作模式如下图:

         

  • 初始向量(IV, Initialization Vector):它的作用和MD5的“加盐”有些类似,目的是防止同样的明文块,始终加密成同样的密文块,以CBC模式为例:

  

 

  在每一个明文块加密前,会让明文块和一个值先做异或操作。IV作为初始化变量,参与第一个明文块的异或,后续的每一个明文块和它前一个明文块所加密出的密文块相异或,从而保证加密出的密文块都不同。

  • 填充方式(padding):由于密钥只能对确定长度的数据块进行处理,而数据的长度通常是可变的,因此需要对最后一块做额外处理,在加密前进行数据填充。常用的模式有PKCS5, PKCS7, NOPADDING
  • 附加消息(AAD,Additional Authenticated Data):附加消息不是重要数据,它只是可以包含在协议中的纯数据,需要对其进行完整性保护,但不需要加密。一个很好的例子是加密IP数据包的标头。如果对它进行加密,则不能将其用于路由;如果不保护它的完整性,则攻击者可能会更改消息的长度或源地址,而收件人却不知道

以上这些构成元素,通常会以参数的形式出现在前后端的加密插件中,在使用前,我们要协商统一,才能完成加解密。

三、AES GCM模式

上文提到了AES工作模式的概念,在介绍GCM之前,我们需要先了解下CTR模式

CTR(Counter Mode,计数器模式):

  

 

  图中可以看出,加密过程使用了密钥、Nonce(类似IV)、Counter(一个从0到n的编号),与上文提及的CBC模式相比,CTR最大的优势是可以并行执行,因为所有的块只依赖于Nonce与Counter,并不会依赖于前一个密文块,适合高速传输需求。但CTR不能提供密文消息完整性校验的功能(未被篡改),所以我们需要引入另一个概念:MAC(消息认证码)。

MAC(Message Authentication Code, 消息认证码):

  是一种用来确认消息完整性并进行认证的技术。通过输入消息与共享密钥,可以生成一段固定长度的数据(MAC值)

  

 

  收发双方需要提前共享一个密钥,发送者使用密钥生成消息的MAC值,并随消息一起发送,接收者通过共享密钥计算收到消息的MAC值,与随附的MAC值做比较,从而判断消息是否被改过(完整性),对于篡改者,由于没有密钥(认证),也就无法对篡改后的消息计算MAC值

GMAC ( Galois message authentication code mode, 伽罗瓦消息验证码 ):

  对应到上图中的消息认证码,GMAC就是利用伽罗华域(Galois Field,GF,有限域)乘法运算来计算消息的MAC值。假设秘钥长度为128bits, 当密文大于128bits时,需要将密文按128bits进行分组。应用流程如下图

    

GCM( Galois/Counter Mode ) :

  GCM中的G就是指GMAC,C就是指CTR。

  GCM可以提供对消息的加密和完整性校验,另外,它还可以提供附加消息的完整性校验。在实际应用场景中,有些信息是我们不需要保密,但信息的接收者需要确认它的真实性的,例如源IP,源端口,目的IP,IV,等等。因此,我们可以将这一部分作为附加消息加入到MAC值的计算当中。下图的Ek表示用对称秘钥k对输入做AES运算。最后,密文接收者会收到密文、IV(计数器CTR的初始值)、MAC值。

    

 

 

 四、前端加密、解密

前端能使用gcm加密的库比较少,先后尝试了 node-forge 和 crypto-js 两个库。其中node-forge提供了AES-GCM加密模式,但是很遗憾,加密之后的密文后端无法解开。crypto-js没有此加密模式。

最终采用了node端使用的库 crypto

npm install --save crypto // 注意不是crypto-js
npm install --save buffer // 如果是 node 端自带buffer,但是浏览器没有,需要引入buffer库支持
import { Buffer } from 'buffer'
import crypto from 'crypto'

/**
 * aes-128-gcm 加密
 * @param {String} msg 加密字符串
 * @param {String} key 密钥
 * @returns 加密后的字符串,16进制
 */
function Encrypt(msg, key) {
  try {
    var pwd = Buffer.from(key, 'hex')
    var iv = crypto.randomBytes(12)
    var cipher = crypto.createCipheriv('aes-128-gcm', pwd, iv)

    var enc = cipher.update(msg, 'utf8', 'hex')
    enc += cipher.final('hex')
    //cipher.getAuthTag() 方法返回一个 Buffer,它包含已从给定数据计算后的认证标签。 
    //cipher.getAuthTag() 方法只能在使用 cipher.final() 之后调用 这里返回的是一个十六进制后的数组
    var tags = cipher.getAuthTag()
    enc = Buffer.from(enc, 'hex')
    // 由于和java对应的AES/GCM/PKCS5Padding模式对应 所以采用这个拼接
    var totalLength = iv.length + enc.length + tags.length
    var bufferMsg = Buffer.concat([iv, enc, tags], totalLength)
    return bufferMsg.toString('hex')
  } catch (e) {
    console.log("Encrypt is error", e)
    return null
  }
}

/**
 * aes-128-gcm 解密
 * @param {String} serect 密文 16进制
 * @param {String} key 密钥 16进制
 * @returns 
 */
function Decrypt(serect, key) {
  try {
    var tmpSerect = Buffer.from(serect, 'hex')
    var pwd = Buffer.from(key, 'hex')
    // 读取数组
    var iv = tmpSerect.slice(0, 12)
    var cipher = crypto.createDecipheriv('aes-128-gcm', pwd, iv)
    // 这边的数据为 去除头的iv12位和尾部的tags的16位
    var msg = cipher.update(tmpSerect.slice(12, tmpSerect.length - 16))
    return msg.toString('utf8')
  } catch (e) {
    console.log("Decrypt is error", e)
    return null
  }
}

如果后端采用 base64形式,只需要将 hex 替换成 base64即可

五、后端Java加、解密

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
 
public class AESUtil {
  public static String parseByte2HexStr(byte buf[]) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < buf.length; i++) {
      String hex = Integer.toHexString(buf[i] & 0xFF);
      if (hex.length() == 1) {
        hex = '0' + hex;
      }
      sb.append(hex.toUpperCase());
    }
    return sb.toString();
  }

  public static byte[] parseHexStr2Byte(String hexStr) {
    if (hexStr.length() < 1)
      return null;
    byte[] result = new byte[hexStr.length() / 2];
    for (int i = 0; i < hexStr.length() / 2; i++) {
      int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
      int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
      result[i] = (byte) (high * 16 + low);
    }
    return result;
  }

  /***
   * aes-128-gcm 加密
   * @params msg 为加密信息 password为32位的16进制key
   * @return 返回base64编码,也可以返回16进制编码
   **/
  public static String Encrypt(String msg, String password) {
    try {
      byte[] sSrc = msg.getBytes("UTF-8"); //修改添加字符集
      byte[] sKey = AESUtil.parseHexStr2Byte(password);
      SecretKeySpec skeySpec = new SecretKeySpec(sKey, "AES");
      Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
      cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
      //这边是获取一个随机的iv 默认为12位的
      byte[] iv = cipher.getIV();
      //执行加密
      byte[] encryptData = cipher.doFinal(sSrc);
      //这边进行拼凑 为 iv + 加密后的内容
      byte[] message = new byte[12 + sSrc.length + 16];
      System.arraycopy(iv, 0, message, 0, 12);
      System.arraycopy(encryptData, 0, message, 12, encryptData.length);

      return Base64.getEncoder().encodeToString(message);
    } catch (Exception ex) {
      return null;
    }
  }

  /***
   * aes-128-gcm 解密
   * @return msg 返回字符串
   */
  public static String Decrypt(String serect, String password) {
    try {
      byte[] sSrc = Base64.getDecoder().decode(serect);
      byte[] sKey = AESUtil.parseHexStr2Byte(password);

      GCMParameterSpec iv = new GCMParameterSpec(128, sSrc, 0, 12);
      Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
      SecretKey key2 = new SecretKeySpec(sKey, "AES");

      cipher.init(Cipher.DECRYPT_MODE, key2, iv);

      //这边和nodejs不同的一点是 不需要移除后面的16位
      byte[] decryptData = cipher.doFinal(sSrc, 12, sSrc.length - 12);
      
      return new String(decryptData);
    } catch (Exception ex) {
      return null;
    }
  }

  public static void main(String[] args) {
    String testMsg = "{'ai':'test-accountId','name':'username','idNum':'371321199012310912'}";
    String testPwd = "10210b07c5cf31b30f722f9b5896de5c";
    String enc = AESUtil.Encrypt(testMsg, testPwd);
    System.out.println("加密结果 " + testPwd);
    String dec = AESUtil.Decrypt(enc, testPwd);
    System.out.println("解密结果 " + dec);
  }
}

 

 参考文章:

  aes-128-gcm 不需要传iv的加密方式和解密

  AES-GCM 加密简介

  node-forge

 

  

 

posted on 2022-06-29 15:07  sjpqy  阅读(8565)  评论(0编辑  收藏  举报

导航