Golang 实现与 crypto-js 一致的 AES 简单加解密

UFO研究
• 阅读 2651

前言

最近一直在折腾 Golang 的 AES 加密解密,最初的一个小需求只是寻求一个简单直接的加密工具而已,但是找着找着发现里面的坑太深了...

吐槽:对于加密解密,其实我们很多时候并没有特别高的要求(复杂)。一开始,我最直接的一个想法就是:

  1. 调用一个方法,传递一个秘钥,完成加密;
  2. 调用一个方法,传递一个秘钥,完成解密,

就可以了,但事实网上纷繁复杂的实现让我头疼。难道,就没有一个让我最省心、简单、最快、实现一个加解密的方法吗?

目标

  1. 我要一个对称加密,加解密用的 key 一致
  2. 加密后的数据 = 加密方法(数据, key)
  3. 解密后的数据 = 解密方法(数据, key)
    仅此而已,但寻变网络各种类库,没意外,各有各的问题,下面我列举几个我在做的过程中遇到的问题和坑

问题

  1. AES 有各种加密模式 CBC、ECB、CTR、OCF、CFB 选哪个?都安全吗?
  2. AES 在某些加密模式下需要指定 IV 也就是初始向量(那我岂不是又要弄一个配置项?)
  3. AES 对于 key 的长度 和 IV 的长度都有要求 (这个很烦,就像我定一个密码还非得是固定长度的)
  4. AES 需要加密的数据不是16的倍数的时候,需要对原来的数据做padding操作(可以简单理解为补充长度到固定的位数)好嘛,padding还有不同的方式:Zero padding、ANSI X.923、PKCS7...
  5. js 常用 crypto-js 进行加密解密操作(我这边还想有个特别需求能保证 js 加密一致)

上代码

show me your code 先来看下最终实现情况如何,然后再来说原理和问题

Golang 实现

package main

import (
    "fmt"

    "github.com/LinkinStars/go-scaffold/contrib/cryptor"
)

func main() {
  key := "1234"

  e := cryptor.AesSimpleEncrypt("Hello World!", key)
  fmt.Println("加密后:", e)
  
  d := cryptor.AesSimpleDecrypt(e, key)
  fmt.Println("解密后:", d)

  iv := cryptor.GenIVFromKey(key)
  fmt.Println("使用的 IV:", iv)
}

// 输出
// 加密后: NHlpzbcTvOj686VaF7fU7g==
// 解密后: Hello World!
// 使用的 IV: 03ac674216f3e15c
对,这就是我想要的,输入需要加密的内容和 key,给我出加密后的结果就好

crypto-js 实现

解密也是类似的,这里我就不重复代码了
import CryptoJS from 'crypto-js'

var data = "Hello World!"
var keys = [
  "1234", 
  "16bit secret key", 
  "16bit secret key1234567", 
  "16bit secret key12345678",
  "16bit secret key16bit secret ke",
  "16bit secret key16bit secret key",
  "16bit secret key16bit secret key1",
]

function aesEncrypt(data, key) {
  if (key.length > 32) {
    key = key.slice(0, 32);
  }
  var cypherKey = CryptoJS.enc.Utf8.parse(key);
  CryptoJS.pad.ZeroPadding.pad(cypherKey, 4);

  var iv = CryptoJS.SHA256(key).toString();
  var cfg = { iv: CryptoJS.enc.Utf8.parse(iv) };
  return CryptoJS.AES.encrypt(data, cypherKey, cfg).toString();
}

for (let i = 0; i < keys.length; i++) {
  console.log(aesEncrypt(data, keys[i]))
}

// 输出
// NHlpzbcTvOj686VaF7fU7g==
// PuMhKY8ZFLnDAwlQ7v/2SQ==
// ZG9JUBvEXrXwSS2RIHvpog==
// pbvDuBOV3tJrlPV0xdmbKQ==
// uAeg71zBzFeUfEMHJqCSxw==
// j9SbFFEEFX4dT9VaDAzsCg==
// j9SbFFEEFX4dT9VaDAzsCg==

问题与解决方案

选择什么加密模式

加密模式有 CBC、ECB、CTR、OCF、CFB,其中 ECB 有安全问题,所以一定不选择,而常用的是 CBC,并且 crypto-js 默认也用了 CBC 所以就无脑选择了 CBC

密钥的长度问题

AES 需要你指定的 密钥长度 必须为 128 位、192 位或256 位,即字符串长度为:16、24 或 32。
对于知道 AES 算法的人来说,其实这很好理解,并且很容易接受,但是对于一个完全不知道你程序或者应用的外部使用者来说,必须写一个长度固定的密码很难理解
所以对与 key(密钥) 我做了如下处理:

  1. 长度超过 32 ,直接截取前面 32
  2. 长度不满足要求的,使用 ZeroPadding 方式补全 (小于 16 的补充到 16,大于 16 小于 24 的补充到 24)

    ZeroPadding 其实实现非常简单,就是将长度不足的末尾补 0 补足就可以

初始向量 IV 的问题

首先来解释为什么需要 IV

Golang 实现与 crypto-js 一致的 AES 简单加解密
其实很好理解,AES 的加密方式是将原数据拆分成一块一块,每一块单独进行加密,最后组合到一起,而在 ECB 模式下,每块加密使用的 key 都是一样的,所以有安全风险,而为了解决这个问题,和 MD5 类似就是给你的加“盐”,我们知道正常的 hash 容易碰撞被猜到,而加了盐之后,相当于给了一个偏移量,使得结果不可被预测。而 CBC 模式下,第一块加密数据所需的这个盐就是 IV,后面几块加密所需的盐都是通过前面来得到的。

那如何创造 IV 呢?

再次从使用者的角度出发,我既然已经提供了一个 key 去加密了,为什么还要提供一个与 key 类似的东西去加密呢?就相当于我需要记住两个密码,很麻烦。并且通常如果作为配置项出现的话,两个 key 肯定是配置在一起的,配置文件里面一般不会为了安全而特别的将两个密码分开存放。

所以我在思考如何创造一个 IV 呢?

首先,肯定这个 IV 需要从 key 出发,因为解密也需要,随机或固定肯定不可能,所以我的第一想法就是 IV 与 key 一致,当然我相信很多人都有和我一样的想法,但是,抱歉,不行。

📢 注意!!!IV 与 key 一致在某些加密模式下相当于你直接将 key 暴露给了用户

所以我参考了老版本 node 的实现,并且改进了一下

The password is used to derive the cipher key and initialization vector (IV). The value must be either a 'latin1' encoded string, a Buffer, a TypedArray, or a DataView.

The implementation of crypto.createCipher() derives keys using the OpenSSL function EVP_BytesToKey with the digest algorithm set to MD5, one iteration, and no salt. The lack of salt allows dictionary attacks as the same password always creates the same key. The low iteration count and non-cryptographically secure hash algorithm allow passwords to be tested very rapidly.

In line with OpenSSL's recommendation to use a more modern algorithm instead of EVP_BytesToKey it is recommended that developers derive a key and IV on their own using crypto.scrypt() and to use crypto.createCipheriv() to create the Cipher object. Users should not use ciphers with counter mode (e.g. CTR, GCM, or CCM) in crypto.createCipher(). A warning is emitted when they are used in order to avoid the risk of IV reuse that causes vulnerabilities. For the case when IV is reused in GCM, see Nonce-Disrespecting Adversaries for details.

老版本 node 里面就直接将 key MD5 了一下作为了 IV,那显然 MD5 是容易被碰撞的。那么好,既然 MD5 不行,那我直接 SHA256 总可以了吧(目前理论安全)。

于是,对于 IV 的生成我就采取了 SHA256 的方式,对 key 做了一次 hash 并且由于 IV 长度固定为 16,所以我又做了一次截取,这下你总不可能还原了吧。

原数据处理模式

上面我们知道,AES 使用 CBC 模式进行加密的时候,需要将数据拆分成一块一块的,那么问题就是,每块长度为 16,当拆分到最后长度不足的时候又需要补充,也叫 padding。padding 还有不同的方式:Zero padding、ANSI X.923、PKCS7...

这里,类似的,由于 crypto-js 默认使用 PKCS7 所以就用它了。

其他问题

  • 我在寻找工具的过程中看过很多方法,发现都会在加密的时候返回 error,我就很难受,我也明白他们返回 error 通常是由于 key 长度不满足要求的时候返回,所以我这里直接处理,当 error 出现直接返回空字符串。
  • crypto-js 在使用的时候一定记得需要使用方法转换 CryptoJS.enc.Utf8.parse 否则会导致加密不一致的情况
  • CryptoJS.pad.ZeroPadding.pad(cypherKey, 4); 这里的 4 的原因是内部方法计算时 乘以了 4,其实是 block 的大小也就是 16,这也是一个坑,不看源码也不知道的坑。我一开始传递的就是 16 😭 源码位置:https://github.com/brix/crypto-js/blob/develop/src/pad-zeropadding.js

总结

代码实现在:
https://github.com/LinkinStars/go-scaffold/blob/main/contrib/cryptor/aes.go
如果需要,你不一定需要直接引用,拷贝对应方法到自己的项目中进行使用就可以了,希望能帮助到你。同时也有支持自定义指定 IV 的方法 AesCBCEncrypt,但相对应的你需要自己去保证 key 和 iv 的长度正确了。

最后要提醒一下,虽然我使用了 crypto-js 进行加密,但由于是业务需要,如果你在使用的话一定要注意不要将 key 给前端页面进行解密,毕竟 AES 是对称加密。

参考链接

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
UFO研究
UFO研究
Lv1
思欲委符节,引竿自刺船。
文章
4
粉丝
0
获赞
0