CryptoJS 使用

4,932 阅读6分钟

CryptoJS

GitHub: github.com/brix/crypto…
文档:cryptojs.gitbook.io/docs/
中文版:yztldxdzhu.github.io/2019/07/23/…


最近做的项目中需要用到加密,在搜索之后,选用了 CryptoJS 来做 AES 加密,下面是自己在学习的过程中,对几个重要概念的笔记,还有一些自己的理解,如有错误之处,还请各位大佬指正。


Hash

所有经过哈希算法之后,得到的都是一个 WordArray 对象,调用 toString 转化为字符串时,默认转成16进制的字符串,也可指定字符串的格式。

使用哈希算法时,传入的参数可以是 String 类型,或者是 CryptoJS.lib.WordArray 实例。当传入的是一个 String 时,会自动转换为一个以 Utf8 编码的 WordArray 对象

let md5 = CryptoJS.MD5('12345')
// 等同于
let md5 = CryptoJS.MD5(CryptoJS.enc.Utf8.parse('12345'))

Cipher

解密之后得到的是 WordArray 对象。

加密之后得到的是 CipherParams 对象,可以从中读取 key, iv, salt, ciphertext,可以调用 toString 方法得到对应的字符串,默认是OpenSSL兼容格式,也可指定字符格式。

加/解密时使用的 key, iv, salt 都是 WordArray 对象

参数 类型
key WordArray
iv WordArray
salt WordArray

Cipher输入

对于明文消息,cipher算法接受字符串或 CryptoJS.lib.WordArray 的实例。

对于密钥(key),当您传递字符串时,它被视为密码并用于派生实际密钥(key)和IV。 或者,您可以传递表示实际密钥(key)的WordArray如果传递实际密钥(key),则必须传递实际的IV。(即都传 WordArray 对象)

对于密文,cipher算法接受字符串或CryptoJS.lib.CipherParams的实例。 CipherParams对象表示一组参数,例如IVsalt和原始密文本身。 传递字符串时,它会根据可配置的格式策略自动转换为CipherParams对象。


个人理解:

加密时,如果密钥(key)直接使用字符串,加密算法会内部根据密钥自动生成实际使用的密钥(WordArray对象),并生成 ivsalt,而生成 ivsalt又会用到一些随机的算法,这样就导致每次加密出来的密文是不一样的,而且必须保留加密过程中产生的 ivsalt,只有使用它们才能解密。

如果密钥使用 WordArray 对象,那么 iv 也必须是 WordArray 对象,这样加密出来的内容就是固定的,同时,指定 iv 时,加密算法便不再自动生成 ivsalt,所以在指定 iv 时,加密后得到的 CipherParams 对象里没有 salt(除非在加密是也指定了 salt)。

另外,如果密钥使用了字符串,也指定了 iv,此时的 iv 是没有用的,每次生成的密文还是不一样的。

注意:实验发现,用做密钥(key)的字符串,长度必须是4的倍数(iv 不做要求),否则在解密时,得到 WordArray 对象后,无法解析出原内容。


Format

加密之后输出的是一个 CipherParams 对象,其中包含了 key, iv, salt, ciphertext,要想获取密文,需要取出 ciphertext 并调用 toString 方法,也可以直接在 CipherParams 对象上调用 toString 方法,但他们得到的值是不一样的。

如果我们想格式化输入的密文的话,就需要指定 format 参数,定义一个 format 对象,里面包含两个方法,stringifyparse

  • stringify:调用加密算法之后,得到 CipherParams 对象,在此对象上调用 toString 方法时,会触发 format 中的 stringify 方法,同时把 CipherParams 对象作为参数传入,取出其中的 ciphertext 对象(也是一个 WordArray),调用它的 toString 方法,同时传入自己需要的编码格式(CryptoJS.enc.Utf8CryptoJS.enc.HexCryptoJS.enc.Base64 等),即可得到对应的密文。对于 ivsalt 也是同样的操作。
  • parse:解密时,当第一个参数是一个字符串时,才会调用到 parse 方法,如果使用 CipherParams 对象则不会触发,使用 parse 的主要目的是为了配合 stringify 方法,在 parse 方法中,解析 stringify 方法产生的字符串,得到对应的 ciphertextivsalt,创建一个 CryptoJS.lib.CipherParams 实例并返回。

对于解密,有两种方式,一种是 decrypt 方法的第一个参数传入字符串,此时会触发 format 中的 parse 方法(如果配置了 format),逻辑见上文; 第二种是直接生成一个 CipherParams 对象,作为第一个参数,这样不会触发parse 方法,但同样需要传入 iv 等参数


例:
// Formatter
let CryptoJSFormat = {
  /**
   * 加密后得到的 CipherParams 对象,调用 toString 时会调用这个方法
   */
  stringify: function(cipherParams) {
    console.log('CryptoJSAESFormat stringify ----------- ')
    // 加密结束,得到 CipherParams 对象,获取其中需要的数据,进行格式化
    var result = {
      ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64) // 将密文转换成 Base64 格式字符串
    };

    if (cipherParams.iv) {
      result.iv = cipherParams.iv.toString(); // 默认转换成16进制字符串
    }

    if (cipherParams.salt) {
      result.salt = cipherParams.salt.toString();
    }

    return JSON.stringify(result);
  },

  /**
   * 解密
   * 在解密开始时便调用,将数据解析并生成 CipherParams 对象
   */
  parse: function(jsonStr) {
    let jsonObj;
    if (typeof jsonStr == 'string') {
      jsonObj = JSON.parse(jsonStr);
    } else {
      jsonObj = jsonStr
    }

    // 获取密文并创建 CipherParams 对象
    let cipherParams = CryptoJS.lib.CipherParams.create({
      ciphertext: CryptoJS.enc.Base64.parse(jsonObj.ct), // 将密文字符串转换成 WordArray 对象
    })

    // 如果有 iv 和 salt ,获取并转换成 WordArray 对象
    if (jsonObj.iv) {
      cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv); // 16 进制字符串转换 WordArray 对象
    }

    if (jsonObj.salt) {
      cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.salt); // 16 进制字符串转换 WordArray 对象
    }

    return cipherParams;
  }
};

使用:

/**
 * AES 加密
 * @param plaintext 明文字符串
 */
export const AES_Encrypt = (plaintext) => {

  let ciphertext = CryptoJS.AES.encrypt(plaintext, kPassphrase, {
    mode: CryptoJS.mode.CFB, // mode 和 padding 的默认值分别为 CBC 和 Pkcs7,加解密时需要保持一致
    padding: CryptoJS.pad.AnsiX923,
    format: CryptoJSAESFormat
  }).toString();
  // console.log(ciphertext);

  return ciphertext;
}

/**
 * AES 解密
 * @param jsonStr
 */
export const AES_Decrypt = (jsonStr) => {
  let plaintext = CryptoJS.AES.decrypt(jsonStr, kPassphrase, {
    mode: CryptoJS.mode.CFB, // mode 和 padding 的默认值分别为 CBC 和 Pkcs7,加解密时需要保持一致
    padding: CryptoJS.pad.AnsiX923,
    format: CryptoJSAESFormat
  }).toString(CryptoJS.enc.Utf8);

  // let jsonObj = JSON.parse(jsonStr);

  // let plaintext = CryptoJS.AES.decrypt(jsonObj.ct, kPassphrase, {
  //   mode: CryptoJS.mode.CFB,
  //   padding: CryptoJS.pad.AnsiX923,
  // });

  // plaintext = plaintext.toString(CryptoJS.enc.Utf8);

  // console.log(plaintext);

  return plaintext;
}

上页的例子中,使用的是字符串密钥,在加密过程中会自动生成真正的密钥和 ivsalt,每次加密出来的密文是不一样的。


test10() {
    // 加密

    const kPassphrase = "com.";
    const ivStr = '123abc'
    let pass = 'superman'

    let key = CryptoJS.enc.Utf8.parse(kPassphrase)
    let iv = CryptoJS.enc.Utf8.parse(ivStr)

    let c = CryptoJS.AES.encrypt(pass, key, {
      iv: iv,
    }).ciphertext.toString(CryptoJS.enc.Base64)

    console.log(c)

    // 解密
    let cipherParams = CryptoJS.lib.CipherParams.create({
      ciphertext: CryptoJS.enc.Base64.parse(c),
    })
    let result = CryptoJS.AES.decrypt(cipherParams, key, {
      iv: iv,
    })
    console.log(result.toString(CryptoJS.enc.Utf8))
  }

key使用 WordArray 对象,此时 iv 也必须使用 WordArray 对象。


Encode

对于 Encode 方法的使用,如果密钥是一个 Utf8 的字符串,就使用 Utf8 的方法来转化 WordArray 对象,如果是一个 16 进制的字符串,就用 Hex 的方法来转化,总之,使用哪个方法取决于字符串是哪种。

let keyStr = 'test2018'
let ivStr = '1234567890abcdef'
let key = CryptoJS.enc.Utf8.parse(keyStr) // 以 utf8 的格式,转化为 WordArray 对象
let iv = CryptoJS.enc.Hex.parse(ivStr) // 以 16进制 的格式,转化为 WordArray 对象

项目中使用

在小程序中使用时,不能使用 npm 方式,安装和构建npm 都可以正常进行,但是在引入到 .js 文件中使用时,会报错。所以还是直接在 GitHub 上下载 release 的包,将文件引入到小程序项目中。


在 Vue 项目中,直接使用 npm 安装,在文件中 import 引入即可使用