把“ 我们都是好朋友”用这个密码本变换之后就得到了这样的结果:
小时候玩这个游戏乐此不疲,以为非常有趣。上大学后,有幸听卢开澄教授讲《打算机密码学》,才知道原来我们小时候玩的这个游戏远远不能称之为加密。那么到底什么是加密呢?
什么是加密?

把字符串 123456经由 base64变换之后,得到了 MTIzNDU2,有人说这是 base64加密。
把字符串 123456经由 md5变换之后,得到了 E10ADC3949BA59ABBE56E057F20F883E,有人说这是 md5加密。
从严格意义上来说,不管是 base64还是 md5乃至更繁芜一些的 sha256都不能称之为加密。
一句话,没有密钥的算法都不能叫加密。
编码(Encoding)是把字符集中的字符编码为指定凑集中某一工具(例如:比特模式、自然数序列、8位字节或者电脉冲),以便文本在打算机中存储和通过通信网络的通报的方法,常见的例子包括将拉丁字母表编码成摩尔斯电码和 ASCII。 base64只是一种编码办法。
杂凑(Hashing)是电脑科学中一种对资料的处理方法,通过某种特定的函数/算法(称为杂凑函数/算法)将要检索的项与用来检索的索引(称为杂凑,或者杂凑值)关联起来,天生一种便于搜索的资料构造(称为杂凑表)。杂凑算法常被用来保护存在资料库中的密码字符串,由于杂凑算法所打算出来的杂凑值具有不可逆(无法逆向演算回原来的数值)的性子,因此可有效的保护密码。常用的杂凑算法包括 md5, sha1, sha256等。
加密(Encryption)是将明文信息改变难堪以读取的密文内容,使之不可读的过程。只有拥有解密方法的工具,经由解密过程,才能将密文还原为正常可读的内容。加密分为对称加密和非对称加密,对称加密的常用算法包括 DES, AES等,非对称加密算法包括 RSA,椭圆曲线算法等。
在古典加密算法当中,加密算法和密钥都是不能公开的,一旦透露就有被破解的风险,我们可以用词频推算等方法获知明文。 1972年美国 IBM公司研制的 DES算法( Data Encryption Standard)是人类历史上第一个公开加密算法但不公开密钥的加密方法,后来成为美国军方和政府机构的标准加密算法。 2002年升级成为 AES算法( AdvancedEncryption Standard),我们本日就从 AES开始入手学习加密和解密。
准备工具
常日情形下,加解密都只须要在做事端完造诣够了,这也是网上大多数教程和样例代码的情形,但在某种分外情形下,你须要用一种措辞加密而用另一种措辞解密的时候,最好有一个中立的公道的第三方结果集来验证你的加密结果,否则一旦出错,你都不知道是加密算法出错了,还是解密算法出错了,对此我们是有惨痛教训的,特殊是如果一个公司里,写加密的是前端,用的是 js措辞,而写解密的是后端,用的是 java措辞或者 php措辞或者 go措辞,则双方更须要有这样一个客不雅观公道的平台,否则你们之间一定会陷入永无休止的相互责怪的田地,前端说自己没有错,是后端解密解错了,后端说解密没有错,是前端加密写错了,而事实上是双方都是菜鸟,对密码学一知半解,在这种情形下摧残浪费蹂躏的韶光就更多。
在线AES加密解密便是这样的一个工具网站,你可以在上面验证你的加密结果,如果你加密得到的结果和它的结果完备同等,就解释你的加密算法没有问题,否则你就去调度,直到和它的结果完备同等为止。反之亦然,如果它能从一个密文解密解出来,而你的代码解不出来,那么一定是你的算法有问题,而不可能是数据的问题。
我们先在这个网站上对一个大略的字符串 123456进行加密。
下面我们对网站上的所有选项逐个阐明一下:
AES加密模式:这里我们选择的是 ECB( ee cc block)模式。这是 AES所有模式中最大略也是最不被人推举的一种模式,由于它的固定的明文对应的是固定的密文,很随意马虎被破解。但是既然是练习的话,就让我们先从最大略的开始。添补:在这里我们选择 pkcs标准的 pkcs7padding。数据块:我们选择 128位,由于 java端解密算法目前只支持 AES128,以是我们先从 128位开始。密钥:由于我们前面选择了 128位的数据块,以是这里我们用 128 / 8 = 16个字节来处理,我们先大略地填入 16个0,实在你也可以填写任意字符,比如 abcdefg1234567ab或者其它,只假如 16个字节即可。理论上来说,不是16个字节也可以用来当密钥,精良的算法会自动补齐,但是为了大略起见,我们先填入 16个 0。偏移量:置空。由于是 ECB模式,不须要 iv偏移量。输出:我们选择 base64编码办法。字符集:这里由于我们只加密英笔墨母和阿拉伯数字,以是选择 utf-8和 gb2312都是一样的。好了,现在我们知道按照以上选项设置好之后的代码如果加密 123456的话,该当输出 DoxDHHOjfol/2WxpaXAXgQ==,如果不是这个结果,那便是加密真个问题。
AES-ECB
AES-ECB的Javascript加密
为了完成 AES加密,我们并不须要自己手写一个 AES算法,不须要去重复造轮子。但如何选择 js的加密库是个很故意思的寻衅。我们考试测验了很多方法,一开始我们考试测验了aes-js这个库,但它不支持 RSA算法,后来我们看到Web Crypto API这种浏览器自带的加密库,原生支持 AES和 RSA,但它的 RSA实现和 Java不兼容,终极我们还是选择了Forge这个库,它天生支持 AES的各种子集,并且它的 RSA也能和 Java完美合营。
利用 forge编写的 js代码实现 AES-ECB加密的代码便是下面这些:
const cipher = forge.cipher.createCipher('AES-ECB', '这里是16字节密钥');cipher.start();cipher.update(forge.util.createBuffer('这里是明文'));cipher.finish();const result = forge.util.encode64(cipher.output.getBytes())
forge的 AES缺省便是 pkcs7padding,以是不用特殊设置。运行它之后你就会得到精确的加密结果。
AES-ECB的Java解密
接下来我们看看Java真个解密代码该如何写:
try {
Cipher cipher = Cipher.getInstance(\公众AES/ECB/PKCS5Padding\公众);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(\公众这里是16字节密钥\公众.getBytes(), \公众AES\"大众));
String plaintext = new String(cipher.doFinal(Base64.getDecoder().decode(\"大众这里是明文\公众.getBytes())), \"大众UTF-8\公众);
System.out.println(plaintext);
} catch (Exception e) {
System.out.println(\"大众解密出错:\"大众 + e.toString());
}
把稳这里我们用到的是 PKCS5Padding,上面加密的时候不是用的是 pkcs7padding吗?怎么这里变成 5了呢?
我们先来理解一下什么是 pkcs。 pkcs的全称是 Public Key Cryptography Standards(公钥加密标准),这是 RSA实验室制订的一系列的公钥密码编译标准,比较著名的有 pkcs1, pkcs5, pkcs7, pkcs8这四个,它们分别管理的是不同的内容。在这里我们只是用它来添补,以是我们只关注 pkcs5和 pkcs7就够了。那么 pkcs5和 pkcs7有什么差异呢?其实在添补方面它们两个的算法是一样的, pkcs5是 pkcs7的一个子集,差异在于 pkcs5是 8字节固定的,而 pkcs7可以是 1到 255之间的任意字节。但用在 AES算法上,由于 AES标准规定块大小必须是 16字节或者 24字节或者 32字节,不可能用 pkcs5的 8字节,以是 AES算法只能用 pkcs7添补。但是由于 java早期工程师犯的一个命名上的缺点,他们把 AES添补算法的名称设定为 pkcs5,而实际实现中实现的是 pkcs7,以是我们在 java端开拓解密的时候须要利用 pkcs5。
AES-CBC
谈完了不屈安的 AES-ECB,我们来做一下相对安全一些的 AES-CBC模式。
AES-CBC的Javascript加密
直接上代码:
const cipher = forge.cipher.createCipher('AES-CBC', '这里是16字节密钥');cipher.start({ iv: '这里是16字节偏移量' });cipher.update(forge.util.createBuffer('这里是明文'));cipher.finish();const result = forge.util.encode64(cipher.output.getBytes());
跟上面的 AES-ECB差不多,唯一差异只是在 start函数里定义了一个 iv。
AES-CBC的Java解密
下面是 Java代码:
try {
Cipher cipher = Cipher.getInstance(\公众AES/CBC/PKCS5Padding\公众);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(\"大众这里是16字节密钥\"大众.getBytes(), \公众AES\"大众), new IvParameterSpec(\公众这里是16字节偏移量\"大众.getBytes()));
String plaintext = new String(cipher.doFinal(Base64.getDecoder().decode(\公众这里是明文\"大众.getBytes())), \"大众UTF-8\"大众);
System.out.println(plaintext);
} catch (Exception e) {
System.out.println(\"大众解密出错:\"大众 + e.toString());
}
也是同样,跟上面用 AES-ECB时的模式险些千篇一律,只是增加了一个 IvParameterSpec,用来天生 iv,在 cipher.init里面增加了一个 iv参数,除此之外完备相同,就这样我们就已经实现了一个大略的 CBC模式。
RSA
以上两种做法都明显是非常不屈安的,由于我们把加密用的密钥和 iv参数都直接暴露在了前端,为此我们须要一种更加安全的加密方法—— RSA。由于 RSA是非对称加密,纵然我们把加密用的公钥完备暴露在前端也不必担心,别人纵然截获了我们的密文,但由于他们没有解密密钥,是无法解出我们的明文的。
天生密钥对
要用 RSA加密,首先我们须要天生一个公钥和一个私钥,我们可以直接实行命令 ssh-keygen。它会问我们密钥文件保存的文件夹,把稳一定要单独找一个文件夹存放,不要放在缺省文件夹下,否则你日常利用的 ssh公钥和私钥就都被覆盖了。
得到公钥文件之后,由于这个公钥文件是 rfc4716格式的,而我们的 forge库哀求一个 pkcs1格式的公钥,以是这里我们须要把它转换成 pem格式(也便是 pkcs1格式):
ssh-keygen -f 公钥文件名 -m pem -e
RSA的Javascript加密
得到 pem格式的公钥之后,我们来看一下 js的代码:
forge.util.encode64(forge.pki.publicKeyFromPem('-----BEGIN RSA PUBLIC KEY-----MIIBCfdsafasfasfafsdaafdsaAB-----END RSA PUBLIC KEY-----').encrypt('这里是明文', 'RSA-OAEP', { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha1.create() } });
一句话就完玉成部加密过程了,这便是 forge的强大之处。
RSA的Java解密
接下来我们看解密。
对付私钥,由于 Java只支持 PKCS8,而我们用 ssh-keygen天生的私钥是 pkcs1的,以是还须要用以下命令把 pkcs1的私钥转换为 pkcs8的私钥:
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in 私钥文件名 -out 导出文件名
得到 pkcs8格式的私钥之后,我们把这个文件的头和尾去掉,然后放入以下 Java代码:
try { Cipher cipher = Cipher.getInstance(\"大众RSA/ECB/OAEPWithSHA-256AndMGF1Padding\公众); cipher.init(Cipher.DECRYPT_MODE, KeyFactory.getInstance(\"大众RSA\"大众).generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(\"大众这里是私钥\公众)))); String plaintext = new String(cipher.doFinal(Base64.getDecoder().decode(\公众这里是密文\"大众.getBytes())), \公众UTF-8\"大众); System.out.println(plaintext);} catch (Exception e) { System.out.println(\"大众解密出错:\"大众 + e.toString());}
和上面的 AES解密类似,只是增加了 KeyFactory读取 PKCS8格式私钥的部分,这样我们就完成了 Java真个 RSA解密。以上我们用最大略的办法实现了 js端加密, java端解密的过程,感兴趣的朋友可以在这里下载完全的代码亲自验证一下: