循序渐进学加密

栏目: 编程工具 · 发布时间: 4年前

内容简介:还记得上初二的那年夏天,班里来了一个新同学,他就住在我家对面的楼里,于是我们一起上学放学,很快便成了最要好的朋友。我们决定发明一套神秘的沟通方式,任何人看到都不可能猜到它的真实含义。我们第一个想到的就是汉语拼音,但很显然光把一个句子变成汉语拼音是不够的,于是我们把26个英文字母用简谱的方式从低音到高音排起来,就得到了一个简单的密码本:把“我们都是好朋友”用这个密码本变换之后就得到了这样的结果:

还记得上初二的那年夏天,班里来了一个新同学,他就住在我家对面的楼里,于是我们一起上学放学,很快便成了最要好的朋友。我们决定发明一套神秘的沟通方式,任何人看到都不可能猜到它的真实含义。我们第一个想到的就是汉语拼音,但很显然光把一个句子变成汉语拼音是不够的,于是我们把26个英文字母用简谱的方式从低音到高音排起来,就得到了一个简单的密码本:

循序渐进学加密

把“我们都是好朋友”用这个密码本变换之后就得到了这样的结果:

循序渐进学加密

小时候玩这个游戏乐此不疲,觉得非常有趣。上大学后,有幸听卢开澄教授讲《计算机密码学》,才知道原来我们小时候玩的这个游戏远远不能称之为加密。那么到底什么是加密呢?

什么是加密?

把字符串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算法(Advanced Encryption 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

1. 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,所以不用特别设置。运行它之后你就会得到正确的加密结果。

2. 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模式。

1. 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。

2. 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是非对称加密,即使我们把加密用的公钥完全暴露在前端也不必担心,别人即使截获了我们的密文,但因为他们没有解密密钥,是无法解出我们的明文的。

1. 生成密钥对

要用RSA加密,首先我们需要生成一个公钥和一个私钥,我们可以直接执行命令ssh-keygen。它会问我们密钥文件保存的文件夹,注意一定要单独找一个文件夹存放,不要放在缺省文件夹下,否则你日常使用的ssh公钥和私钥就都被覆盖了。

得到公钥文件之后,由于这个公钥文件是rfc4716格式的,而我们的forge库要求一个pkcs1格式的公钥,所以这里我们需要把它转换成pem格式(也就是pkcs1格式):

ssh-keygen -f 公钥文件名 -m pem -e 

2. 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的强大之处。

3. 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端解密的过程,感兴趣的朋友可以在这里下载完整的代码亲自验证一下:

https://github.com/fengerzh/encdec


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Building Social Web Applications

Building Social Web Applications

Gavin Bell / O'Reilly Media / 2009-10-1 / USD 34.99

Building a social web application that attracts and retains regular visitors, and gets them to interact, isn't easy to do. This book walks you through the tough questions you'll face if you're to crea......一起来看看 《Building Social Web Applications》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具