加密解密

作为一个底层码农,其实加密解密平时是用不到的,毕竟平时写的就是cms 的curd, 但随着API的流行以及老大对代码质量的重视,还有和别的部门的交互,于是需要加解密啦。原文

hash( hamc ) 和 加解密

hash(hamc) 可以用的算法: md5, sha1, sha256, sha512

加解密:base64, aes

用途:

hash 和 hamc : 因为不能解密,所以一般用在签名过程中,怎么验证是否数据被用户修改了呢,用相同的密钥对于传递过来的数据重新加密一遍,和sign 比对,如果相等,那就没有人修改签名(注意这个密钥一定不能被知道,因为一旦被知道,劫持者可以根据密钥帮修改后的数据生成对应sign, 起不到保护的作用)

这篇文章是关于hamc ,hash 和md5 sha1 关系的讲解

md5: 经常用到,有时候我们需要固定长度的字符串,就可以用md5加密一下,md5 可以生成32位或者64位,依据不同的算法,js 好像经常出64位的,和php对接的时候要注意一下

sha1: 感觉和上面的md5类似(感觉就是两种不同的加密算法)

老李文章中对sha1 的描述:

其实跟前面的MD5同期的还有一个SHA1加密方式的,不过也是算法比较简单,所以这里就一笔带过吧。而这里即将要说到的SHA256SHA512都是来自于SHA2家族的加密函数,看名字可能你就猜的出来了。

hmac: hmac 其实就是用上面两种算法(sha256, md5),再加上一个密钥进行加密(这个密钥感觉又类似salt 的存在)

1
2
3
4
5
6
7
8
php中md5, sha1 都有直接内置的方法, md5(), sha1(), 用的都是hash 方式,可以如下验证
md5('cy') === hash('md5', 'cy') // true
sha1('cy') === hash('sha1', 'cy') // true

sha256, sha512 php 没有内置的方法,需要借助函数 hash('sha256', data) 。
可以通过hash_algos 查看hash() 支持哪些算法。

hash_hmac 是另一种类似 hash 的函数,也支持 md5, sha256, https://www.php.net/manual/zh/function.hash-hmac.php, 常用函数,同样也可以通过 hash_hmac_algos 查看支持哪些算法

hmac 比较官方的描述

散列消息身份验证码 Hashed Message Authentication Code 。它不是散列函数,而是采用了将MD5或SHA1散列函数与共享机密秘钥(与公钥/秘钥对不同)一起使用的消息身份验证机制。消息与秘钥组合并运行散列函数(md5或sha1),然后运行结果与秘钥组合并再次运行散列函数。

HMAC-SHA1简要来说,就是采用sha1算法,与HMAC机制相结合,制造出更加难以破解的加密串。

hash_hmac

在php中hash_hmac函数就能将HMAC和一部分哈希加密算法相结合起来实现HMAC-SHA1 HMAC-SHA256 HMAC-MD5等等算法。函数介绍如下:

string hash_hmac(string $algo, string $data, string $key, bool $raw_output = false)

algo:要使用的哈希算法名称,可以是上述提到的md5,sha1等

data:要进行哈希运算的消息,也就是需要加密的明文。

key:使用HMAC生成信息摘要是所使用的密钥。

raw_output:该参数为可选参数,默认为false,如果设为true,则返回原始二进制数据表示的信息摘要,否则返回16进制小写字符串格式表示的信息摘要(注意是16进制数,而非简单的字母加数字)。

php 中我们常用的还有下面的密码散列算法,四个方法

其中password_hash 默认算法是bcrypt (也不知道这个密码散列和上面的hash 有啥区别,从目前的bcrypt 上来看,就是把hash次数和salt 的内容都放在加密字符串中)

bcrypt

bcrypt : 关于bcrypt 这里有个解释很清楚, 感觉上其实就是另一种算法的hash

我之前很好奇这种hash, 怎么验证,也不能解密呀,当看到上面这张图片的时候,完全明白了,他的salt 存在了他加密后的字符串中,有了salt 就和以前的md5加解密一样了(注意上面各个字段的意思,10代表hash 10次),php现在经常用的一种加密密码的方式也很好用

1
2
3
4
5
md5(‘123456’) e10adc3949ba59abbe56e057f20f883e 
md5(‘123456’ . ($salt = ‘salt’)) 207acd61a3c1bd506d7e9a4535359f8a
sha1(‘123456’) 40位密文
hash(‘sha256’, ‘123456’) 64位密文 // sha2
hash(‘sha512’, ‘123456’) 128位密文 // sha2
1
2
3
4
5
6
7
$str = password_hash(123, PASSWORD_DEFAULT);  //默认 bcrypt, PASSWORD_DEFAULT 也代表了 PASSWORD_BCRYPT.二者都是1
password_verify(123, $str); // 验证

password_hash() – 对密码加密.
password_verify() – 验证已经加密的密码,检验其hash字串是否一致.
password_needs_rehash() – 给密码重新加密. // 其实感觉没啥必要,主要是因为感觉algo或者cost hash次数改变就会改变
password_get_info() – 返回加密算法的名称和一些相关信息.

这篇文章php的password api说的挺好的

urlencode

关于urlencode ,这篇文章说到的蛮好的,但是太长了 看看阮一峰老师的文章 , 我大致理解了下,首先php只有一个urlencode , 但是js 有两种,分别是 encodeUri 和 encodeUriComponent, php 的 相当于js 的后者,后者在加密url的时候会破坏url自身的完整性,他只适用于对url中的参数进行加密,因为会对:// 都进行转义,这样就不能当做url请求了

(ps: urlencode 啥作用,比如我发送了一个请求 http://www.baidu.com?url=http://chenye.com&name=11, 我希望的是url参数 中携带着后面的name ,而不是name 和 url 同级别的关系,这时候只能用urlencode 加密特定的参数啦, 记得在express 中好像默认会帮助我们decodeUriComponent 关于http url部分)

加解密:数据

base64:

感觉我平时用到最多的地方就是不能识别的二进制(乱码)转换成base64 或者 16进制 hex。

比如之前itbasic 上面图片转换成base64然后传给后端,后端php 再把base64解析成二进制,写入文件,保存成图片。

如果不用base64 上传图片我们还可以通过post 提交,http 协议是纯文本协议,我们怎么上传二进制的图片呢, http 有自己的机制,http 头部的mime 头也设定了该文件是图片,可以百度下怎么用node stream实现一个静态web 服务器。

php中很多加解密都是先生成二进制,然后转换成 base64 或者 16进制 (或者默认生成的就是 base64 )

(因为二进制可能含有不可见字符,复制粘贴都不方便)

这个评论更加深了纯文本协议的理解

base64_encode

base64_decode

base64 会让传输内容变大, https://www.zhihu.com/question/36306744, base64 出现原理和使用,这篇文章讲解的比较全面 (这个我就在图片处理的时候用到过,一些算法,比如aes, js 有 hex 或者 base64 的方式,感觉就是为了让过滤掉二进制中的特殊字符)

这种最原始的方式,因为不需要密钥,感觉不能算上加解密,确实在各个网站上也是这么感觉的

首先的区分下加解密和hash 的不同,对于md5() 这样加密之后理论上就不能解密的方式,我们一般称之为hash,在我们以往的网站编程中,对于用户的密码保存一般都是通过hash这种方式,通过把用户的密码拼接一个字符串,然后md5下生成的值保存到数据库中,下次用户登陆的时候,再次把用户输入的密码做上次同样的操作,对比和数据库中值是否一致,如果一致说明密码正确,否则密码错误。

加密我们平时生活中遇到最多的应该就是https 解析过程,因为非对称加密太消耗性能,所以我们传输过程需要对称加密(对称加密就是加密密钥和解密密钥一致,非对称反之),而对于密钥的传输我们用的是非对称(非对称因为密钥的不同又有公钥和私钥之分:公钥加密,私钥解密,数据不会被获取;私钥加密,公钥解密,数据不会被篡改)。

对称加密算法一般有AES(itbasic 请求datrix 那边用的就是这种,ecb 没有$iv 偏移量,cbc 有偏移量 ),DES,3DES,非对称有RSA

上图中,123456 就是对称加密过程中的密钥

最一开始的时候,我朝人民一般都是倾向于使用“天王盖地虎”,“宝塔镇河妖”这种加解密技术;然而,美帝用了一种叫做DES的技术进行对称加解密,这玩意一度成为业界通用的对称加解密技术,银行、五角大楼都爱用这玩意,可惜好景不长、世风日下、世态炎凉,这玩意的破解成本越来越低越来越低~~ 于是,为了续命,就又有一些白胡子老头给DES打补丁,缝缝补补搞出来一个玩意叫做3DES,继续用,又不是不能用…这个顾名思义就行了,别打我,真的:3DES就是用DES处理(注意是处理,我没说是加密)了三次的意思。就目前看来,3DES实际上用的可能也并不是十分广泛了,所以如果大家在选择对称加解密技术的时候,尽量避开DES和3DES就可以了。

呵呵,喜新厌旧的沙雕人类…虽然DES已经没人用了,但毕竟也是辉煌过,我觉得还是得动手表演一波儿。我们知道,在php7里,原来的mcrypt系列加解密已经被放弃掉了,官方建议我们使用openssl系列来进行加解密,所以确保你的PHP环境里安装了openssl标准扩展。

上面是原文中对于DES 和 3DES 的描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这个函数打印出来openssl支持的所有加密方法以及模式的组合
$arr_ava_methods = openssl_get_cipher_methods(true);
print_r( $arr_ava_methods );
exit;
// 返回值
[123] => aes-192-cfb8
[124] => aes-192-ctr
[125] => aes-192-ecb
[126] => aes-192-gcm
[127] => aes-192-ocb
[128] => aes-192-ofb
[129] => aes-256-cbc
[130] => aes-256-ccm
[131] => aes-256-cfb
[132] => aes-256-cfb1
[133] => aes-256-cfb8
[134] => aes-256-ctr

其中带有ede的,比如des-ede*这样的就表示是3DES

其中des 有如下几种

1
2
3
4
des-cbc
des-cfb*(注意后面的通配符星号)
des-ecb
des-ofb

下面是如何通过des-ecb 的方式加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 我们就选用des-ecb方法进行一次des加密
$ava_methods = openssl_get_cipher_methods();
$my_method = 'des-ecb';
if ( !in_array( $my_method, $ava_methods ) ) {
exit( '错误的加密方法'.PHP_EOL );
}
$key = "123456";
$data = "helloMOTO";
echo "明文:".$data.PHP_EOL;
$enc_data = openssl_encrypt( $data, $my_method, $key );
echo "密文:".$enc_data.PHP_EOL;
$dec_data = openssl_decrypt( $enc_data, $my_method, $key );
echo "明文:".$dec_data.PHP_EOL;

// 输出
明文:helloMOTO
密文:IbpWiZbAoNMiknX3jdeqmQ==
明文:helloMOTO

接下来我们说说aes 的加密方式

1
2
3
4
5
6
7
8
9
10
11
12
13
$ava_methods = openssl_get_cipher_methods();
// 选用aes-128-ecb
$my_method = 'aes-128-ecb';
if ( !in_array( $my_method, $ava_methods ) ) {
exit( '错误的加密方法'.PHP_EOL );
}
// 加密用的密码
$key = "1234567812345678";
// 加密的内容
$data = "12345678abcdxxoo12345678abcdxxoo";
$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA );
$hex = bin2hex( $enc_data );
echo $hex.' : '.strlen( $hex ).PHP_EOL;

aes-128-ecb, 其中128代表密钥的长度,多余的长度会被直接街去掉(这个可以通过代码验证下,比如12345 用密钥1234567812345678和12345678123456789分别加密,看结果是否一样,最后再用不满128bit来加密123456781234567 ),128bit

128/8=16 位,aes-192-ecb, aes-256-ecb, 这其中的192 ,256,都是同样的意思。

最后一个参数是OPENSSL_RAW_DATA,如果选用这个option的话,经过加密后的数据会是奇怪的二进制数据,无法直接通过文本方式查看,所以要看的话必须先使用bin2hex函数处理一下。

上面这段话一定要注意理解,反正我第一次没有理解,导致第一次加密数据失败,网上的很多aes-128-ecb 这种加密代码的封装中都携带着自己的逻辑,所以会误导视野。上述opensssl_encrypt(),如果没有用最后一个参数,直接返回的结果是加密结果base64 的结果,如果有了,返回的是一段奇怪的二进制数据数据,这里我们可以理解成原始数据(加密数据最原始的样子),我们可以通过base64或者 bin2hex 转换城对应的base64格式或者16进制格式

1
2
3
4
5
6
7
8
9
10
11
12
明文:
12345678abcdxxoo12345678abcdxxoo
密文:c1391e34caf38f8c2a477cbda3772533c1391e34caf38f8c2a477cbda3772533d96aa42b59151a9e9b5925fc9d95adaf
// 上面密文切分的结果
c1391e34caf38f8c2a477cbda3772533 | c1391e34caf38f8c2a477cbda3772533 | d96aa42b59151a9e9b5925fc9d95adaf

难道说明文“12345678abcdxxoo”被密钥“1234567812345678”加密后后的密文就是“c1391e34caf38f8c2a477cbda3772533”?

DES和3DES会将明文以64bit(8字节)作为一个单元进行分组;
AES则会将明文以128bit(16字节)作为一个单元进行分组;
无论是AES还是DES,当最后一个分组的数据长度不满足分组标准长度的时候,会用某种填充方式进行填充;
AES对一个16字节分组加密完毕后,分组大小依然为16字节;DES也一样

上面就是aes ecb 的处理过程

如果我们把加密的内容分组之后稍微修改下分组顺序再解密,可以得到原文分组顺序的不同

ecb太不靠谱了,竟然能修改,出现了cbc 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ava_methods = openssl_get_cipher_methods();
$my_method = 'aes-128-cbc';
if ( !in_array( $my_method, $ava_methods ) ) {
exit( '错误的加密方法'.PHP_EOL );
}
// 密钥 和 明文
$key = "1234567812345678";
$data = "12345678abcdxxooxxooabcd12345678i";
// 每种方法都有自己需要的iv向量的长度
$iv_length = openssl_cipher_iv_length( $my_method );
// 根据长度生成相应iv
$iv = openssl_random_pseudo_bytes( $iv_length, $cstrong ); // 我就是一直把这玩意当成一个随机的字符串看待的,他真的就是一个随机字符串
echo "明文:".$data.PHP_EOL;
$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA, $iv );
$dec_data = openssl_decrypt( $enc_data, $my_method, $key, OPENSSL_RAW_DATA, $iv );
echo "解密:".$dec_data.PHP_EOL;

// 神器的代码
test2(1,$c); // 这样竟让不会报错
function test2 ($a, &$b) {
var_dump($a+$b, $b);
}

exit;

今天和我司员工对接cbc模式,~~不出意外,这条产品线的小伙伴也是在网上copy网上代码,结果直接没有给我iv向量,没有iv向量怎么可以解密呢,哈哈哈,其实之前我也不知道解密需要iv,我以为这个iv是包自带的,不需要我们管,但是当我在网上的在线平台使用的时候,发现cbc必须填写iv向量,最少还是16个字节,也就是16个英文字符(吐槽一下php自身生成的那个应该是二进制,需要转换一下,否则根本复制不下来,复制下来也不是16个字符长度),怎么办呢,这时候我逐渐意识到是不是加解密还需要iv,老李代码中反正有,

1
2
$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA, $iv ); // 加密
$dec_data = openssl_decrypt( $enc_data, $my_method, $key, OPENSSL_RAW_DATA, $iv ); // 解密

其实老李上面那段代码也写了注释,把iv当做一个随机的字符串,看来真的可能需要iv,这时候就开始翻他的go代码,但是他的go代码中没有传递iv变量这个值呀,看到了下面

1
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])

~~对了,就是这个 这个key[:blocksS],这个就是iv,怪不得他不用传递,原来是约定的,

初始化向量(IV)

http://blog.csdn.net/xiaohu50/article/details/51682849

初始化向量(IV,Initialization Vector)是许多工作模式中用于随机化加密的一块数据,因此可以由相同的明文,相同的密钥产生不同的密文,而无需重新产生密钥,避免了通常相当复杂的这一过程。

初始化向量与密钥相比有不同的安全性需求,因此IV通常无须保密,然而在大多数情况中,不应当在使用同一密钥的情况下两次使用同一个IV。对于CBC和CFB,重用IV会导致泄露明文首个块的某些信息,亦包括两个不同消息中相同的前缀。对于OFB和CTR而言,重用IV会导致完全失去安全性。另外,在CBC模式中,IV在加密时必须是无法预测的;特别的,在许多实现中使用的产生IV的方法,例如SSL2.0使用的,即采用上一个消息的最后一块密文作为下一个消息的IV,是不安全的。

因为刚做了一个接口的用户信息加解密,加密至URL,然后那边后台解析,一开始IV不一致老是有一个参数解析不了,后来问同事是IV的问题,所以在这边做个记录,对这个还不是很了解。。。

如果加解密的IV不一致的话会导致数据不一致,所以解密失败,IV如果不变的话会导致不安全

上面是我在百度go cbc 加解密的时候看到的, 起初还以为go的cbc加密iv有什么特殊化,看来是我多虑了,就像很多时候aes加密本身不需要base64,但很多时候网上代码都加上了,因为默认约定这样好看点,或者字符长度确定了便于存储

这个传说中的iv向量终于他妈出现了!相对于ECB模式,CBC在加密之前多了一个XOR异或运算的环节,但是第一个明文分组和谁做异或呢?所以这个iv向量就是初始化后给第一个明文分组做XOR异或运算用的,第二个明文分组就与第一个密文分组做XOR异或运算,然后再加密得到第二个密文分组…依次重复下去。

xor 就是异或,相同的取0,不同的取1

总结:

AES和DES以及3DES这种加密方式被称为分组密码,分组密码每次只能加密固定长度的明文,所以如果明文很长的话,就需要轮流为每个分组明文进行加密,AES的分组长度是128bit,而DES的分组长度为64bit;如果一旦需要对多个分组进行轮流加密,加入明文被分成了三个明文分组,那么就需要对三个明文进行迭代加密(粗暴理解就是轮流加密),然而会有很多种不同的迭代方式,这种不同的迭代方式专业名词就叫“模式”,这些模式有:ECB、CBC、OFB、CFB、CTR… …

对明文进行分组的方式是固定的,唯一不同的就是分组长度不一样而已;模式是指对多个明文从第一个开始轮流加密到最后一个的这个过程,是怎么轮流执行的。

非对称加解密就不copy了,以后用到的时候再看吧

自己的总结:

其实上面用到的也就怎么用php 进行aes-128-ecb加密,加密出来的数据让别的语言可以解析出来

这其中会遇到很多问题,比如之前曾经遇到过但是没有记录的和java通信过程中的padding 填充问题,以后遇到了再补充吧。

顺带补充一下:

https://www.devglan.com/online-tools/aes-encryption-decryption

这个网站以及下面的php代码

1
2
3
4
5
6
7
8
9
10
11
12
$data = '陈野';
$key = '1234567812345678';

echo $key.PHP_EOL;

$enc_data = openssl_encrypt( $data, $my_method, $key, OPENSSL_RAW_DATA);
echo $enc_data.PHP_EOL;


echo base64_encode($enc_data).PHP_EOL;

echo bin2hex( $enc_data );

默认的填充方式

pkcs5padding or pkcs7padding

https://blog.csdn.net/Stewart/article/details/52462273

这篇文章是关于pkcs5 和 7 padding 的比较,填充的是什么内容也有具体说。(感觉php 就是 5填充,因为没法指定blockSize的大小)

字符集(utf8)

http://tool.chacuo.net/cryptaes

上面这个网站才是真正意义上详细的加密,参数可以选择

今天着重研究了下pkcs5padding 和 pkcs7padding , 网上说的 5p 是7 p的子集, 7p的填充 1 到255 字节都可以, 5p的填充只能 1 到 8字节 ,可是我发现aes 是16字节分组的(不管是 128 还是 256 算法,都是16字节分组),按理说5p只能填充 1到8 那如果只有1个字符的话,怎么填充呢,是填充7个字节还是15字节呢,反正,当我填充15的时候php的openssl nopading 出来的数据和 openssl raw data 出来的数据是一样的,那么应该就是填充15了

网上看到7p和5p的区别大致上都是:5p是7p子集,5p只支持到8的block,7p可以支持到255,所以

这篇文章上说的aes 5p和7p是一样的, 感觉是错的,就是aes没有5p的padding

关于nopadding, 字面意思上就是不填充,对于aes,不填充的话,必须保证是16字节整数倍,否则会加密失败

关于zeropadding, php的 openssl 虽然有这个参数,但是要是不满16字节,仍然是会报错,既然这样,zeropadding 感觉和nopadding 就差不多了,实践了一下,果然zero_padding 就是base64了一下no_padding的结果

关于老版本的加密函数的转换,据说是老版本因为是zero_padding 导致数据出来和新版本不一样,zero_padding ,在末尾填充的是 chr(0), 代表0 这个ascii 对应的 字符,很多网上写的是 \0(很奇怪的是chr(\0) 能得到和 chr(0) 一样的字符,但是这个字符我们键盘是不能表示出来的, chr(\1) 和 chr(1) 却不一样) ,当我们拼凑到16字节之后,再解密,只需要过滤掉\0这个特殊字符就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 这是php 的7padding ,
function pad($data, $blocksize = 16)
{
$pad = $blocksize - (strlen($data) % $blocksize);
return $data . str_repeat(chr($pad), $pad);
}

// 因为最后7padding 最后一个字符肯定是填充的自己长度,所以只需要截取未填充的部分就好
function unpad($text)
{
$pad = ord($text[strlen($text) - 1]); // 填充的字符
// var_dump($pad);

if ($pad > strlen($text))
{

return false;
}

if (strspn($text, chr($pad), strlen($text) - $pad) != $pad)
{
return false;
}

return substr($text, 0, -1 * $pad);
}

注意一下填充时候的length, 我们计算的时候都是用的strlen, 并不是 中文字符的len

注意一下,我们没填充之前,已经是一个完整的string, 我们每次填充的内容,都是我们肉眼可以识别的字符,或者说是我们可以处理的字符,所以我们可以裁剪substr, 或者trim过滤掉我们添加的字符

总结:hash 和 加解密大致就是上面那些东西了,后面jwt 这些认证方式,其实就是 hash 和 加解密的实际运用,本质上并不是一个新的东西

现在重看, 阮一峰 老师的这篇jwt,讲解的真的好,jwt首先就可以看做一个token,自带token 相比较传统的cookie session,更便于扩展,由客户端自己维持登陆态。缺点token 签发了没法主动让他失效,除非到达失效时间,或者像我们一样添加额外逻辑,token 必须在redis中有对应内容,才能生效。

jwt token ,是由三个字符串base64加密而成, 因为base64解密不需要密钥,所以我们很容易根据base64解密出来

第一段是 alg + type. 算法 + 类型(JWT)

第二段是我们自定义的一个数组,当然可能有些属性是必须的,比如签发时间和过期时间

第三段是sign,为了防止第二段被别人修改,利用的加密算法是前面的alg (可能更确切的说是hash)

综上:所以事先起来感觉很简单,前面两段都是base64, 就是最后一个用hash,之后再base64, 有个问题就是这块的base64不是普通的base64, base64之后再替换三个字符,我的感觉是怕用户把这个字段连接在url后面当做get请求发送,base64中的参数影响到这个url的正确性

当我们明白过程之后,我们就很容易自己实现一个jwt,就不需要用网上的加密包了,我们可以看看php的 firebase/php-jwt 包,蛮简单的,默认hsh256, php 的hash_hmac 很容易实现。

到此为止 ,我们就剩rs256 公钥和私钥没有去研究,以后遇到了再去研究这个firebase的包(从下面也可以看出,hash_hmac 用来hash的函数,openssl 一般是用来加密的函数)

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束感谢您的阅读-------------