【关于前端加密】CryptoJS与blake2b实践

环境:前端vue3,服务端go

需求1:const hashed = (mac+symbol+timestamp)的blake2b128 hash值

需求2:使用aescbc-128算法加密token、body

需求3:解密接口返回的response data

blake2b

  1. 使用 npm 或者 yarn 安装依赖

npm install blake2b

或者

bashCopy codeyarn add blake2b

  1. 在 Vue 3 项目中引入库:

import blake2b from 'blake2b';

  1. 在需要使用 Blake2b-128 哈希的地方,调用库进行计算:
js 复制代码
// 计算 Blake2b-128 哈希 
const hash = blake2b(16).update('Hello, World!').digest('hex'); 
console.log(hash)

💡
在 blake2b(16) 中的 16 表示生成的 Blake2b 哈希的输出长度(以字节为单位)。在这种情况下,16 表示输出的哈希值长度为 16 字节,即 128 位
Blake2b 是一种多功能哈希函数,它可以生成不同长度的哈希值。通过指定不同的输出长度,你可以控制生成的哈希值的位数。例如,如果你想要一个 256 位的哈希值,你可以使用 blake2b(32),其中 32 表示输出长度为 32 字节(256 位)。

在使用 Blake2b 哈希库时,根据你的具体需求,你可以选择适当的输出长度来满足你的要求。请注意,输出长度越长,哈希值的唯一性和强度可能会更高,但也会增加存储和传输的开销。

AES-CBC-128加密算法

使用CryptoJS 库来实现

  1. 使用 npm 或 yarn 安装 CryptoJS:

npm install crypto-js

或者

yarn add crypto-js

  1. 在需要使用 AES-CBC-128 加密的地方,引入库:

import CryptoJS from 'crypto-js';

  1. 使用 CryptoJS 进行 AES-CBC-128 加密:
js 复制代码
// 定义密钥和初始向量(示例) 
const key = CryptoJS.enc.Hex.parse('000102030405060708090a0b0c0d0e0f'); // 128-bit key 
const iv = CryptoJS.enc.Hex.parse('101112131415161718191a1b1c1d1e1f'); // 128-bit IV 

// 加密 
const plaintext = 'Hello, World!'; 
const encrypted = CryptoJS.AES.encrypt(plaintext, key, { iv: iv }).toString(); 

console.log(encrypted);

在上述示例中,我们使用 CryptoJS 库进行 AES-CBC-128 加密。我们提供了一个 128 位的密钥和一个 128 位的初始向量(IV),并使用 CryptoJS.AES.encrypt 方法进行加密操作。加密结果以字符串形式输出。

实操

以上是两个工具的基本用法,接下来进入实践部分

前提:const ac = 与服务端约定一个常量

目标:在请求头中添加session

aescbc-128加密(计算session)

const ac = "aaabbbccc"

token: "aaabbbccc"

  1. ac+timestamp(时间戳),计算blake2b128得到hash值
  2. 使用aescbc-128算法加密token,加密key为hashed前16个字符转byte数组(Uint8Array),iv为后16个字符转byte数组
  3. 将加密后的结果做base64,即为session
js 复制代码
// 转换为byte数组
const strToByteArray = (str: string) => {
  return new Uint8Array([...str].map((c) => c.charCodeAt(0)))
}

// 计算 Blake2b-128 哈希
export const calcBlake2b = (val: string) => {
  return blake2b(16).update(strToByteArray(val)).digest('hex')
}

// 模拟go的HexDecode(js版本) 
// 每两位转为一个十进制数字   比如 :  d4 --- >  212
const HexDecode = (hexString: string) => {
  const decimalArray = []

  for (let i = 0; i < hexString.length; i += 2) {
    const hexPair = hexString.substr(i, 2)
    const decimalNumber = parseInt(hexPair, 16)
    decimalArray.push(decimalNumber)
  }
  return new Uint8Array(decimalArray)
}

// 计算session
export const geneOpenApiSession = (
  ac: string,
  token: string,
  timestamp: string
) => {
  const hashed = calcBlake2b(ac + timestamp)
  // hashed前16个字符转换为byte数组 128-bit key
  const keyBytes = strToByteArray(hashed.substring(0, 16))
  // hashed后16个字符转换为byte数组 128-bit IV
  const ivBytes = strToByteArray(hashed.substring(hashed.length - 16))

  // aescbc-128算法
  const plaintext = CryptoJS.enc.Hex.parse(token) // 同:CryptoJS.lib.WordArray.create(HexDecode(token))
  const encrypted = CryptoJS.AES.encrypt(plaintext, CryptoJS.lib.WordArray.create(keyBytes), {
    iv: CryptoJS.lib.WordArray.create(ivBytes)
  }).toString()

  return encrypted
}

CryptoJS.enc.Hex.parse

💡
CryptoJS.enc.Hex.parse是 CryptoJS 库中的一个方法,用于将十六进制字符串转换为 CryptoJS 所使用的 WordArray 对象。

对于 AES 加密算法,输入数据通常是 WordArray 类型。WordArray 是 CryptoJS 中用于表示二进制数据的数据结构。

示例

js 复制代码
const CryptoJS = require('crypto-js'); 
const hexString = '48656c6c6f2c20576f726c6421'; 
// 将十六进制字符串转换为 WordArray 对象 
const wordArray = CryptoJS.enc.Hex.parse(hexString); 
console.log(wordArray);

CryptoJS.lib.WordArray.create

💡
CryptoJS.lib.WordArray.create 可以将字节数组(Uint8Array)转换为 CryptoJS 的 WordArray 对象。

直接使用 new Uint8Array 创建的字节数组作为初始向量(iv)或密钥(key),在 CryptoJS 中可能会导致报错。这是因为 CryptoJS 的加密方法期望的是一个 CryptoJS 的WordArray 对象作为初始向量,而不是原始的字节数组。

CryptoJS.enc.Utf8.parse

💡
CryptoJS.enc.Utf8.parse 是 CryptoJS 中的一个方法,用于将 UTF-8 编码的字符串转换为 WordArray 对象。

示例

js 复制代码
const CryptoJS = require('crypto-js'); 
const str = 'Hello, World!'; // 将 UTF-8 编码的字符串转换为 WordArray 对象 
const wordArray = CryptoJS.enc.Utf8.parse(str); 
console.log(wordArray);

💡CryptoJS.AES.encrypt参数说明

  • message:要加密的消息,可以是字符串或 WordArray 对象。CryptoJS.AES.encrypt 方法是用于加密字符串或字节数组的,如果您想要加密一个对象,您需要先将对象转换为字符串或字节数组,然后再进行加密。
  • key:加密密钥。可以是字符串、WordArray 对象或 WordArray 可转换为字符串的格式(如十六进制字符串)。

密钥的长度必须为 128 位(16 字节),因为 AES-CBC-128 使用 128 位的密钥。如果密钥长度不符合要求,将无法正确执行加密和解密操作。

  • options:加密选项。一个包含 iv(初始向量)等属性的对象。
    • iv:初始向量的长度也必须为 128 位(16 字节),与密钥长度相同。初始向量用于增加加密的随机性和安全性。
    • padding(填充):CryptoJS.pad.ZeroPadding 表示使用 Zero Padding(零填充)模式进行数据填充。当要加密的数据长度不是分组长度的整数倍时,需要进行填充操作以满足分组加密的要求。Zero Padding 是一种常见的填充方式,它在数据的末尾添加零字节,使数据长度变为分组长度的整数倍。
    • mode(加密模式):CryptoJS.mode.CBC 表示使用 CBC(Cipher Block Chaining,密码分组链接)模式进行加密。CBC 是一种常用的分组密码模式,它通过将前一个加密分组的密文与当前分组的明文进行异或运算,增加了密码的随机性和安全性。在 CBC 模式中,还需要提供一个初始化向量(IV)作为额外的参数
  • 返回值(加密结果):一个 CipherParams 对象。

加密对象示例

js 复制代码
const CryptoJS = require('crypto-js');

const key = 'your-key'; // 密钥
const iv = 'your-iv'; // 初始化向量

// 要加密的对象
const objectToEncrypt = { name: 'John', age: 30 };

// 将对象转换为字符串
const jsonString = JSON.stringify(objectToEncrypt);

// 将字符串转换为 WordArray 对象
const data = CryptoJS.enc.Utf8.parse(jsonString);

// 使用 CryptoJS 进行 AES 加密
const encrypted = CryptoJS.AES.encrypt(data, key, { iv }).toString();

console.log(encrypted);

WordArray

💡

WordArray 对象是 CryptoJS 中用于表示字节序列的抽象。它提供了一系列方法和功能,用于在加密、解密和其他操作中处理字节序列

js 复制代码
// 将 WordArray 对象还原为字节数组 
const byteArray = Array.from(wordArray.sigBytes === 0 ? \[] : new Uint8Array(wordArray.words.buffer));

其他加密插件:

在 JavaScript 中,有多个库可以用于实现 AES-CBC-128 算法加密。以下是一些常用的库:

  1. CryptoJS:CryptoJS 是一个流行的加密库,它提供了丰富的加密算法支持,包括 AES-CBC。您可以使用 CryptoJS.AES.encrypt 函数来进行 AES-CBC-128 加密。它是一个纯 JavaScript 实现的库,易于使用。
    • 可以通过 npm 安装:

npm install crypto-js

  1. Forge:Forge 是一个强大的加密和密码学库,提供了多种加密算法的实现,包括 AES-CBC。您可以使用 forge.cipher.createCipher 函数进行 AES-CBC-128 加密。
    • 可以通过 npm 安装:

npm install node-forge

  1. SJCL:Stanford Javascript Crypto Library(SJCL)是一个全功能的加密库,支持多种加密算法,包括 AES-CBC。它旨在提供安全可靠的加密功能。
    • 可以通过 npm 安装:

npm install sjcl

  1. WebCrypto API:现代浏览器提供了 WebCrypto API,它是一个原生的加密 API,支持多种加密算法,包括 AES-CBC。使用 WebCrypto API 可以在浏览器中进行高性能的加密操作。

在 JavaScript 社区中,CryptoJS 是最常用和广泛使用的加密库之一。它有许多用户和开发者使用它来进行数据加密和解密操作。

CryptoJS 提供了简单易用的 API,并支持多种常见的加密算法,包括 AES、DES、3DES、RC4、SHA 等。它的使用方式灵活,适合用于各种项目和应用场景。是一个成熟且稳定的库,拥有广泛的文档和社区支持。相对而言,CryptoJS 在 JavaScript 社区中的知名度和使用率更高。

aescbc-128解密(解析res.data)

const ac = "aaabbbccc"

token: "aaabbbccc"

  1. const aeskeystr := Blake128Calc([]byte(ac + timestamp)) // timestamp

  2. 解密body,解密key为 HexDecode(aeskeystr),iv为HexDecode(token)

接口返回二进制流:

设定responseType:'arraybuffer'

  • 将 ArrayBuffer 转换为 Uint8Array
js 复制代码
if (res.request.responseType === 'arraybuffer') { 
    const uint8Array = new Uint8Array(res.data) // 将 ArrayBuffer 转换为 Uint8Array 
    return decryptApiBody(ac, token, String(timestamp), uint8Array) 
}
js 复制代码
export const decryptApiBody = (
  ac: string,
  token: string,
  timestamp: string,
  data: any
) => {
  const aeskeystr = calcBlake2b(ac + timestamp)
  const key = CryptoJS.enc.Hex.parse(aeskeystr)
  const iv = CryptoJS.enc.Hex.parse(token)
  // data: Uint8Array转为字符串
  const ciphertext = CryptoJS.enc.Base64.stringify(CryptoJS.lib.WordArray.create(data))

  // 解密
  const decrypted = CryptoJS.AES.decrypt(ciphertext, key, { iv })

  // 解密结果直接转为Uint8Array会出现多余字符,导致JSON.parse失败,所以先转为Base64

  // 将解密结果转换为 Base64 字符串
  const decryptedBase64 = decrypted.toString(CryptoJS.enc.Base64)
  // 将 Base64 字符串转换为 Uint8Array
  const decodedString1 = window.atob(decryptedBase64) // base64解码
  const decryptedUint8Array = new Uint8Array(decodedString1.length)
  for (let i = 0; i < decodedString1.length; i++) {
    decryptedUint8Array[i] = decodedString1.charCodeAt(i)
  }

  // 将 Uint8Array 转换为字符串
  const textDecoder = new TextDecoder('utf-8')
  const decodedString = textDecoder.decode(decryptedUint8Array)

  return decodedString ? JSON.parse(decodedString) : {}
}

CryptoJS.enc.Base64.stringify

💡
CryptoJS.enc.Base64.stringify 方法将 Uint8Array 密文数据转换为 Base64 编码的字符串。

Uint8Array不能直接作为解密参数。转为WordArray也不行

CryptoJS.AES.encrypt(....).toString()

💡

toString默认行为是将加密后的数据转换为base64编码的字符串。

toString方法还接受一个可选的参数,用于指定输出格式。

例如,如果你想要将加密结果转换为Hex字符串:

decrypted.toString(CryptoJS.enc.hex)

但在有些情况下,toString()结果与encrypted.toString(CryptoJS.enc.Base64)不同,这可能与传入的message格式有关 ?(存疑)

  • 例如这个例子中,在加密时encrypted.toString()、encrypted.toString(CryptoJS.enc.Base64)、encrypted.toString(CryptoJS.enc.hex)三种形式输出结果相同
  • 在解密时encrypted.toString()与encrypted.toString(CryptoJS.enc.hex)结果相同,与encrypted.toString(CryptoJS.enc.Base64)不同
    💡坑点

将解密后的数据decrypted转换为 Uint8Array 的话,decodedString会出现多余字符串,导致JSON.parse失败:

猜测是由于:

如果将 Uint8Array 直接转换为字符串,可能会产生额外的字符,这是因为字符串的构造方式和编码方式的差异。

在 JavaScript 中,使用默认的字符串构造函数将 Uint8Array 转换为字符串时,它会尝试将字节序列解释为 UTF-16 编码的字符。如果 Uint8Array 中的字节序列无法正确解码为有效的 UTF-16 字符,则会出现乱码或额外的字符。

所以先把decrypted转为base64,再转Uint8Array

相关推荐
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客2 小时前
pinia在vue3中的使用
前端·javascript·vue.js
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
我码玄黄4 小时前
THREE.js:网页上的3D世界构建者
开发语言·javascript·3d
爱喝水的小鼠4 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
小晗同学4 小时前
Vue 实现高级穿梭框 Transfer 封装
javascript·vue.js·elementui
WeiShuai4 小时前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
forwardMyLife4 小时前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
mez_Blog6 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川6 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试