sm-crypto和js-base64配合进行sm4加密

491 阅读5分钟

需求

需求很简单,如下图所示,要求用sm4加密,最后输出要是base64加密过的。

企业微信截图_20240614184831.png

sm-crypto

sm-crypto是国密算法sm2、sm3和sm4的js实现。用法非常简单。 sm4的加密用法如下:

const sm4 = require('sm-crypto').sm4
const msg = 'hello world! 我是 juneandgreen.' // 可以为 utf8 串或字节数组
const key = '0123456789abcdeffedcba9876543210' // 可以为 16 进制串或字节数组,要求为 128 比特

let encryptData = sm4.encrypt(msg, key) // 加密,默认输出 16 进制字符串,默认使用 pkcs#7 填充(传 pkcs#5 也会走 pkcs#7 填充)
let encryptData = sm4.encrypt(msg, key, {padding: 'none'}) // 加密,不使用 padding
let encryptData = sm4.encrypt(msg, key, {padding: 'none', output: 'array'}) // 加密,不使用 padding,输出为字节数组
let encryptData = sm4.encrypt(msg, key, {mode: 'cbc', iv: 'fedcba98765432100123456789abcdef'}) // 加密,cbc 模式

用法非常简单。sm4运算模式默认是“ecb”,填充模式默认是“pkcs7”,所以这里都不需要配置。需要做的是直接直接转化为base64即可。

js-base64

js-base64也是一个非常简单且好用的base64加解密的npm包。 加密用法如下:

let latin = 'dankogai';
let utf8  = '小飼弾'
let u8s   =  new Uint8Array([100,97,110,107,111,103,97,105]);
Base64.encode(latin);             // ZGFua29nYWk=
Base64.encode(latin, true);       // ZGFua29nYWk skips padding
Base64.encodeURI(latin);          // ZGFua29nYWk
Base64.btoa(latin);               // ZGFua29nYWk=
Base64.btoa(utf8);                // raises exception
Base64.fromUint8Array(u8s);       // ZGFua29nYWk=
Base64.fromUint8Array(u8s, true); // ZGFua29nYW which is URI safe
Base64.encode(utf8);              // 5bCP6aO85by+
Base64.encode(utf8, true)         // 5bCP6aO85by-
Base64.encodeURI(utf8);           // 5bCP6aO85by-

我们最经常用的就是直接Base64.encode('string类型'),通常是utf-8编码的。

sm-crypto和js-base64结合实现需求

如果我们使用默认的加密方式sm4.encrypt(msg, key)那得到的就是一个16进制串,再用js-base64加密的时候就按照了utf8格式的进行加密了,所以,我们需要sm4输出的是字节数组,然后js-base64加密的也是字节数组。 代码如下:

const result = sm4.encrypt(value, publicKey, { output: 'array' });
const base64Res = Base64.fromUint8Array(new Uint8Array(result), false);

补充

ArrayBuffer,二进制数组

基本的二进制对象是 ArrayBuffer —— 对固定长度的连续内存空间的引用。

ArrayBuffer 与 Array 没有任何共同之处:它的长度是固定的,我们无法增加或减少它的长度。

Uint8Array —— Uint8Array 数组类型表示一个 8 位无符号整型数组,创建时内容被初始化为 0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。

Uint16Array —— 表示 16 位无符号整数,按平台字节顺序排列。如果需要控制字节顺序,请使用 DataView 代替。内容被初始化为 0。建立后,就可以使用对象的方法或使用标准数组索引语法(即使用括号表示法)引用数组中的元素。

Uint32Array —— 表示一个由基于平台字节序的 32 位无符号字节组成的数组。如果需要对字节顺序进行控制,请使用 DataView 代替。数组中每个元素的初始值都是0。一旦创建,你可以用对象的方法引用数组里的元素,或者使用标准的数组索引语法(即,使用中括号)。

16进制串和字节数组的相互转换

16进制串转字节数组

function hexToArray(str) {
  const arr = []
  for (let i = 0, len = str.length; i < len; i += 2) {
    arr.push(parseInt(str.substr(i, 2), 16))
  }
  return arr
}

字节数组转16进制串

function ArrayToHex(arr) {
  return arr.map(item => {
    item = item.toString(16)
    return item.length === 1 ? '0' + item : item
  }).join('')
}

utf8串和字节数组的相互转换

utf8串转字节数组

function utf8ToArray(str) {
  const arr = []

  for (let i = 0, len = str.length; i < len; i++) {
    const point = str.codePointAt(i);

    if (point <= 0x007f) {
      arr.push(point);
    } else if (point <= 0x07ff) {
      arr.push(0xc0 | (point >>> 6)); // 110yyyyy(0xc0-0xdf)
      arr.push(0x80 | (point & 0x3f)); // 10zzzzzz(0x80-0xbf)
    } else if (point <= 0xD7FF || (point >= 0xE000 && point <= 0xFFFF)) {
      arr.push(0xe0 | (point >>> 12)); // 1110xxxx(0xe0-0xef)
      arr.push(0x80 | ((point >>> 6) & 0x3f)); // 10yyyyyy(0x80-0xbf)
      arr.push(0x80 | (point & 0x3f)); // 10zzzzzz(0x80-0xbf)
    } else if (point >= 0x010000 && point <= 0x10FFFF) {
      i++;
      arr.push((0xf0 | (point >>> 18) & 0x1c)); // 11110www(0xf0-0xf7)
      arr.push((0x80 | ((point >>> 12) & 0x3f))); // 10xxxxxx(0x80-0xbf)
      arr.push((0x80 | ((point >>> 6) & 0x3f))); // 10yyyyyy(0x80-0xbf)
      arr.push((0x80 | (point & 0x3f))); // 10zzzzzz(0x80-0xbf)
    } else {
      arr.push(point);
      throw new Error('input is not supported');
    }
  }
  return arr;
}

字节数组转utf8串

function arrayToUtf8(arr) {
  const str = [];
  for (let i = 0, len = arr.length; i < len; i++) {
    if (arr[i] >= 0xf0 && arr[i] <= 0xf7) {
      str.push(String.fromCodePoint(((arr[i] & 0x07) << 18) + ((arr[i + 1] & 0x3f) << 12) + ((arr[i + 2] & 0x3f) << 6) + (arr[i + 3] & 0x3f)));
      i += 3;
    } else if (arr[i] >= 0xe0 && arr[i] <= 0xef) {
      str.push(String.fromCodePoint(((arr[i] & 0x0f) << 12) + ((arr[i + 1] & 0x3f) << 6) + (arr[i + 2] & 0x3f)));
      i += 2;
    } else if (arr[i] >= 0xc0 && arr[i] <= 0xdf) {
      str.push(String.fromCodePoint(((arr[i] & 0x1f) << 6) + (arr[i + 1] & 0x3f)));
      i++;
    } else {
      str.push(String.fromCodePoint(arr[i]));
    }
  }
  return str.join('');
}