资讯专栏INFORMATION COLUMN

循序渐进学加密

Y3G / 1108人阅读

摘要:在古典加密算法当中,加密算法和密钥都是不能公开的,一旦泄露就有被破解的风险,我们可以用词频推算等方法获知明文。年美国公司研制的算法是人类历史上第一个公开加密算法但不公开密钥的加密方法,后来成为美国军方和政府机构的标准加密算法。

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

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

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

什么是加密?

把字符串123456经过base64变换之后,得到了MTIzNDU2,有人说这是base64加密。

把字符串123456经过md5变换之后,得到了E10ADC3949BA59ABBE56E057F20F883E,有人说这是md5加密。

从严格意义上来说,不管是base64还是md5甚至更复杂一些的sha256都不能称之为加密。

一句话,没有密钥的算法都不能叫加密。

编码(Encoding)是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位字节或者电脉冲),以便文本在计算机中存储和通过通信网络的传递的方法,常见的例子包括将拉丁字母表编码成摩尔斯电码和ASCIIbase64只是一种编码方式。

杂凑(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个字节来处理,我们先简单地填入160,其实你也可以填写任意字符,比如abcdefg1234567ab或者其它,只要是16个字节即可。理论上来说,不是16个字节也可以用来当密钥,优秀的算法会自动补齐,但是为了简单起见,我们先填入160

偏移量:置空。因为是ECB模式,不需要iv偏移量。

输出:我们选择base64编码方式。

字符集:这里因为我们只加密英文字母和阿拉伯数字,所以选择utf-8gb2312都是一样的。

好了,现在我们知道按照以上选项设置好之后的代码如果加密123456的话,应该输出DoxDHHOjfol/2WxpaXAXgQ==,如果不是这个结果,那就是加密端的问题。

AES-ECB AES-ECB的Javascript加密

为了完成AES加密,我们并不需要自己手写一个AES算法,不需要去重复造轮子。但如何选择js的加密库是个很有意思的挑战。我们尝试了很多方法,一开始我们尝试了aes-js这个库,但它不支持RSA算法,后来我们看到Web Crypto API这种浏览器自带的加密库,原生支持AESRSA,但它的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())

forgeAES缺省就是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了呢?

我们先来了解一下什么是pkcspkcs的全称是Public Key Cryptography Standards公钥加密标准),这是RSA实验室制定的一系列的公钥密码编译标准,比较著名的有pkcs1, pkcs5, pkcs7, pkcs8这四个,它们分别管理的是不同的内容。在这里我们只是用它来填充,所以我们只关注pkcs5pkcs7就够了。那么pkcs5pkcs7有什么区别呢?其实在填充方面它们两个的算法是一样的,pkcs5pkcs7的一个子集,区别在于pkcs58字节固定的,而pkcs7可以是1255之间的任意字节。但用在AES算法上,因为AES标准规定块大小必须是16字节或者24字节或者32字节,不可能用pkcs58字节,所以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 -m PEM。它会问我们密钥文件保存的文件夹,注意一定要多带带找一个文件夹存放,不要放在缺省文件夹下,否则你日常使用的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端解密的过程,感兴趣的朋友可以在这里下载完整的代码亲自验证一下:

https://github.com/fengerzh/e...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104584.html

相关文章

  • 循序渐进加密

    摘要:在古典加密算法当中,加密算法和密钥都是不能公开的,一旦泄露就有被破解的风险,我们可以用词频推算等方法获知明文。年美国公司研制的算法是人类历史上第一个公开加密算法但不公开密钥的加密方法,后来成为美国军方和政府机构的标准加密算法。 还记得上初二的那年夏天,班里来了一个新同学,他就住在我家对面的楼里,于是我们一起上学放学,很快便成了最要好的朋友。我们决定发明一套神秘的沟通方式,任何人看到都不...

    fsmStudy 评论0 收藏0
  • Linux课程适合0基础吗?要习哪些内容?

    摘要:课程从基础入门开始教学,学习难度循序渐进,由浅入深,即使是零基础的学习者也可以完全能够听懂。   想要从事IT行业,但是有不想要学习编程该选择哪门技术合适呢?当然是Linux运维了。Linux是市场上非常受欢迎的技术,应用范围广泛,就业前景好,受到了很多人的喜欢。那么问题来了,Linux运维零基础可以学习吗?  在服务器市...

    高胜山 评论0 收藏0
  • 循序渐进编程

    摘要:对于每个问题几乎任何时候都有不同的解决方法,学着用不同的方法解决问题,对比它们之间的优点和弊端,使用诸如模块化和系统集成的方式编程,因为那样写代码非常的简洁清晰。做里程碑做测试证明做进度规划。 软件开发人员是一个日新月异的领域—–IT中的大师,今天的编程方式与明天的编程或许截然不同,技术在不断地革新,新语言、新平台的如雨后春笋般出现、更好的解决方案的冒出,因此我们需要跟得上节奏,我们别...

    cuieney 评论0 收藏0
  • 循序渐进爬虫:多线程+队列爬取豆瓣高分计算机类书籍

    摘要:上一次的抓取豆瓣高分计算机书籍的案例,采用的是完全同步的方式。是用来进行多线程编程的,也就是用来创建队列。同时这个函数也是由多个解析线程执行。 上一次的抓取豆瓣高分计算机书籍的案例,采用的是完全同步的方式。即单个线程依次执行完所有的逻辑,这样存在的问题就是我们的爬虫程序会非常的慢。 所以本文作为上一次案例的升级版本,通过循序渐进、动手实践的方式来达到更好的学习效果。 相对于上次的案例,...

    blastz 评论0 收藏0

发表评论

0条评论

Y3G

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<