目录
一、编码算法
1.1、ASCII
1.1.1、ASCII简介
1.1.2、ASCII产生原因
1.1.3、表达方式
1.1.4、标准表
1.1.5、大小规则
1.2、Unicode
1.2.1简介
1.2.2编码和实现
1.3、汉字编码
1.3.1、GB2312-80 标准
1.3.2、GBK 编码标准
1.3.3、GB18030编码标准
1.4、URL编码
1.4.1 编码
1.4.2 解码
1.5、Base 64
二、Hash 算法
2.1、 哈希碰撞
2.2、常用哈希算法
2.3、Java标准库提供的哈希算法
2.4、哈希算法用途
2.5、哈希算法存储用户口令
2.6、SHA-1
三、Hmac算法
四、对称加密算法
4.1对称加密算法
4.2 AES ECB工作模式加密
4.3 AES CCB工作模式加密
五、口令加密算法
六、秘钥交换算法
七、非对称加密算法
八、签名算法
九、数字证书
一、编码算法
1.1、ASCII
1.1.1、ASCII简介
ASCII ( American Standard Code Information Interchange ) :美国信息交换标准代码。
ASCII是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语与其他西欧语言。它是最通用的信息交换标准,并等同于国际标准 ISO/IEC 646。 ASCII 第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符。
1.1.2、ASCII产生原因
在计算机中,所有数据在存储和运算时都是使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如52个字母(大小写)以及0到9等数字还有一些常用的符号(*、#、@)在计算机也需要用二进制数来表示,而具体哪些二进制数字表示那个符号,当然每个人都可以约定自己的一套(编码),而大家如果想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了 ASCII 编码,统一规定了上述常用符号用那些二进制表示。
1.1.3、表达方式
ASCII 码使用指定 7 位 或 8 位 二进制数组合来表示 128 或 256 中可能的字符。标准 ASCII 码也叫基础 ASCII 码,使用 7 位二进制数(剩下1位二进制为0) 来表示所有的大写和小写字母,数字0到9、标点符号,以及美式英语中使用的特殊控制字符。
0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符)
如控制字符 LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等。
通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等。
ASCII 值为8、9、10和13分别为转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。
32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。
65~90为 26 个大写英文字母,97~122为 26个小写英文字母,其余为一些标点符号、运算符号等。
需要注意的是,在标准 ASCII 中,最高位(b7)用作奇偶校验位。
奇偶校验:指在代码传送过程中用来验证是否出现错误的一种方法,一般分为奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7 添1;偶数校验规定:正确的代码一个字节中的1的个数必须是偶数,若非偶数,则最高位b7添加1。
后128个称为扩展ASCII码,许多基于x86 的系统都支持使用扩展(或“高”)ASCII 。扩展 ASCII 码允许将每个字符的第8位用于确定附加的128个特殊符号字符、外来语字母和图形符号。
1.1.4、标准表
ASCII 码表具体如下:
Bin (二进制) | Oct (八进制) | Dec (十进制) | Hex (十六进制) | 缩写/字符 | 解释 |
0000 0000 | 00 | 0 | 0x00 | NUL(null) | 空字符 |
0000 0001 | 01 | 1 | 0x01 | SOH(start of headline) | 标题开始 |
0000 0010 | 02 | 2 | 0x02 | STX (start of text) | 正文开始 |
0000 0011 | 03 | 3 | 0x03 | ETX (end of text) | 正文结束 |
0000 0100 | 04 | 4 | 0x04 | EOT (end of transmission) | 传输结束 |
0000 0101 | 05 | 5 | 0x05 | ENQ (enquiry) | 请求 |
0000 0110 | 06 | 6 | 0x06 | ACK (acknowledge) | 收到通知 |
0000 0111 | 07 | 7 | 0x07 | BEL (bell) | 响铃 |
0000 1000 | 010 | 8 | 0x08 | BS (backspace) | 退格 |
0000 1001 | 011 | 9 | 0x09 | HT (horizontal tab) | 水平制表符 |
0000 1010 | 012 | 10 | 0x0A | LF (NL line feed, new line) | 换行键 |
0000 1011 | 013 | 11 | 0x0B | VT (vertical tab) | 垂直制表符 |
0000 1100 | 014 | 12 | 0x0C | FF (NP form feed, new page) | 换页键 |
0000 1101 | 015 | 13 | 0x0D | CR (carriage return) | 回车键 |
0000 1110 | 016 | 14 | 0x0E | SO (shift out) | 不用切换 |
0000 1111 | 017 | 15 | 0x0F | SI (shift in) | 启用切换 |
0001 0000 | 020 | 16 | 0x10 | DLE (data link escape) | 数据链路转义 |
0001 0001 | 021 | 17 | 0x11 | DC1 (device control 1) | 设备控制1 |
0001 0010 | 022 | 18 | 0x12 | DC2 (device control 2) | 设备控制2 |
0001 0011 | 023 | 19 | 0x13 | DC3 (device control 3) | 设备控制3 |
0001 0100 | 024 | 20 | 0x14 | DC4 (device control 4) | 设备控制4 |
0001 0101 | 025 | 21 | 0x15 | NAK (negative acknowledge) | 拒绝接收 |
0001 0110 | 026 | 22 | 0x16 | SYN (synchronous idle) | 同步空闲 |
0001 0111 | 027 | 23 | 0x17 | ETB (end of trans. block) | 结束传输块 |
0001 1000 | 030 | 24 | 0x18 | CAN (cancel) | 取消 |
0001 1001 | 031 | 25 | 0x19 | EM (end of medium) | 媒介结束 |
0001 1010 | 032 | 26 | 0x1A | SUB (substitute) | 代替 |
0001 1011 | 033 | 27 | 0x1B | ESC (escape) | 换码(溢出) |
0001 1100 | 034 | 28 | 0x1C | FS (file separator) | 文件分隔符 |
0001 1101 | 035 | 29 | 0x1D | GS (group separator) | 分组符 |
0001 1110 | 036 | 30 | 0x1E | RS (record separator) | 记录分隔符 |
0001 1111 | 037 | 31 | 0x1F | US (unit separator) | 单元分隔符 |
0010 0000 | 040 | 32 | 0x20 | (space) | 空格 |
0010 0001 | 041 | 33 | 0x21 | ! | 叹号 |
0010 0010 | 042 | 34 | 0x22 | " | 双引号 |
0010 0011 | 043 | 35 | 0x23 | # | 井号 |
0010 0100 | 044 | 36 | 0x24 | $ | 美元符 |
0010 0101 | 045 | 37 | 0x25 | % | 百分号 |
0010 0110 | 046 | 38 | 0x26 | & | 和号 |
0010 0111 | 047 | 39 | 0x27 | ' | 闭单引号 |
0010 1000 | 050 | 40 | 0x28 | ( | 开括号 |
0010 1001 | 051 | 41 | 0x29 | ) | 闭括号 |
0010 1010 | 052 | 42 | 0x2A | * | 星号 |
0010 1011 | 053 | 43 | 0x2B | + | 加号 |
0010 1100 | 054 | 44 | 0x2C | , | 逗号 |
0010 1101 | 055 | 45 | 0x2D | - | 减号/破折号 |
0010 1110 | 056 | 46 | 0x2E | . | 句号 |
0010 1111 | 057 | 47 | 0x2F | / | 斜杠 |
0011 0000 | 060 | 48 | 0x30 | 0 | 字符0 |
0011 0001 | 061 | 49 | 0x31 | 1 | 字符1 |
0011 0010 | 062 | 50 | 0x32 | 2 | 字符2 |
0011 0011 | 063 | 51 | 0x33 | 3 | 字符3 |
0011 0100 | 064 | 52 | 0x34 | 4 | 字符4 |
0011 0101 | 065 | 53 | 0x35 | 5 | 字符5 |
0011 0110 | 066 | 54 | 0x36 | 6 | 字符6 |
0011 0111 | 067 | 55 | 0x37 | 7 | 字符7 |
0011 1000 | 070 | 56 | 0x38 | 8 | 字符8 |
0011 1001 | 071 | 57 | 0x39 | 9 | 字符9 |
0011 1010 | 072 | 58 | 0x3A | : | 冒号 |
0011 1011 | 073 | 59 | 0x3B | ; | 分号 |
0011 1100 | 074 | 60 | 0x3C | < | 小于 |
0011 1101 | 075 | 61 | 0x3D | = | 等号 |
0011 1110 | 076 | 62 | 0x3E | > | 大于 |
0011 1111 | 077 | 63 | 0x3F | ? | 问号 |
0100 0000 | 0100 | 64 | 0x40 | @ | 电子邮件符号 |
0100 0001 | 0101 | 65 | 0x41 | A | 大写字母A |
0100 0010 | 0102 | 66 | 0x42 | B | 大写字母B |
0100 0011 | 0103 | 67 | 0x43 | C | 大写字母C |
0100 0100 | 0104 | 68 | 0x44 | D | 大写字母D |
0100 0101 | 0105 | 69 | 0x45 | E | 大写字母E |
0100 0110 | 0106 | 70 | 0x46 | F | 大写字母F |
0100 0111 | 0107 | 71 | 0x47 | G | 大写字母G |
0100 1000 | 0110 | 72 | 0x48 | H | 大写字母H |
0100 1001 | 0111 | 73 | 0x49 | I | 大写字母I |
01001010 | 0112 | 74 | 0x4A | J | 大写字母J |
0100 1011 | 0113 | 75 | 0x4B | K | 大写字母K |
0100 1100 | 0114 | 76 | 0x4C | L | 大写字母L |
0100 1101 | 0115 | 77 | 0x4D | M | 大写字母M |
0100 1110 | 0116 | 78 | 0x4E | N | 大写字母N |
0100 1111 | 0117 | 79 | 0x4F | O | 大写字母O |
0101 0000 | 0120 | 80 | 0x50 | P | 大写字母P |
0101 0001 | 0121 | 81 | 0x51 | Q | 大写字母Q |
0101 0010 | 0122 | 82 | 0x52 | R | 大写字母R |
0101 0011 | 0123 | 83 | 0x53 | S | 大写字母S |
0101 0100 | 0124 | 84 | 0x54 | T | 大写字母T |
0101 0101 | 0125 | 85 | 0x55 | U | 大写字母U |
0101 0110 | 0126 | 86 | 0x56 | V | 大写字母V |
0101 0111 | 0127 | 87 | 0x57 | W | 大写字母W |
0101 1000 | 0130 | 88 | 0x58 | X | 大写字母X |
0101 1001 | 0131 | 89 | 0x59 | Y | 大写字母Y |
0101 1010 | 0132 | 90 | 0x5A | Z | 大写字母Z |
0101 1011 | 0133 | 91 | 0x5B | [ | 开方括号 |
0101 1100 | 0134 | 92 | 0x5C | \ | 反斜杠 |
0101 1101 | 0135 | 93 | 0x5D | ] | 闭方括号 |
0101 1110 | 0136 | 94 | 0x5E | ^ | 脱字符 |
0101 1111 | 0137 | 95 | 0x5F | _ | 下划线 |
0110 0000 | 0140 | 96 | 0x60 | ` | 开单引号 |
0110 0001 | 0141 | 97 | 0x61 | a | 小写字母a |
0110 0010 | 0142 | 98 | 0x62 | b | 小写字母b |
1.1.5、大小规则
常见ASCII 码的大小规则:数字 < 大写字母 < 小写字母
数字比字母小 。如 “7” < "F"
数字0比数字9要小,并按0到9顺序递增。如 “3”<“8”
字母A要比字母Z要小,并按A到Z顺序递增。如 "A" < "Z"
同个字母的大写要比小写字母小32。
几个常见字母 ASCII 码的大小 "A"为65 ,"a"为97 ,"0"为48
1.2、Unicode
1.2.1简介
统一码(Unicode)也叫万国码,由统一码联盟开发,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。
统一码是为了解决传统的字符编码方案的局限性而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
1990年开始研发,1994年正式发布1.0版本,2022年9月13日发布15.0版本
如果把各种文字编码形容为各地的方言,那么统一码就是世界各国合作开发的一种语言。
在这种语言环境下,不会再有语言的冲突,在同屏下,可以显示任何语言内容,这就是统一码的最大好处。就是讲世界上所有的文字用2个字节统一进行编码。那样,像这样的编码,2个字节就已经足够容纳世界上所有语言的大部分文字了。
Universal Multiple-Octet Coded Character Set,简称为UCS。
现在用的是USC-2,即2 个字节编码,而USC-4是为了防止将来2个字节不够用才开发出来的。
统一编码是一种在计算机上使用的字符编码,1990年开始研发,1994年正式公布,随着计算机工作能力的增强,统一编码也在面世以来的十多年来里得到普及。
统一编码是基于通用字符(Universal Character Set)的标准来发展的,同时也以出版物的形式对外发表。
2005年3月31日推出4.1.0版本
2022年9月13日推出15.0版本
1.2.2编码和实现
大概来说,统一码编码系统可以分为编码方式和实现方式两个层面。
编码方式
统一码是国际组织制定的可以容纳世界上所有文字和字符编码方案。统一码用数字0-0x10FFFF来映射这些字符。最多可容纳1114112个字符,或者说有1114112个码位,码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数字的编码方案。
统一码字符集可以简写为UCS(统一码Character Set) 。早期的统一码标准有UCS-2、UCS-4的说法。UCS-2用两个字节编码,UCS-4用4个字节编码。
实现方式
在统一码中,汉字的“字”对应的数字是23383。在统一码中,我们有很多方式将23383表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF是 UCS Transformation Format可以翻译成统一码字符集转换格式,即怎样将统一码定义的数字转换成程序数据。
1.3、汉字编码
目前的文字编码标准主要用ASCII、GB2312、BGK、Unicode等。ASCII编码是最简单 的西文编码方案。GB2312、GBK、GB18030是汉字字符编码方案的国家标准。ISO/IEC 10646 都是全球字符编码的国际标准。
1.3.1、GB2312-80 标准
GB2312-80 是 1980 年制定的中国汉字编码国家标准。共收录 7445 个字符,其中汉字 6763 个。GB2312 兼容标准 ASCII码,采用扩展 ASCII 码的编码空间进行编码,一个汉字占用两个字节,每个字节的最高位为 1。具体办法是:收集了 7445 个字符组成 94*94 的方阵,每一行称为一个“区”,每一列称为一个“位”,区号位号的范围均为 01-94,区号和位号组成的代码称为“区位码”。区位输入法就是通过输入区位码实现汉字输入的。将区号和位号分别加上 20H,得到的 4 位十六进制整数称为国标码,编码范围为 0x2121~0x7E7E。为了兼容标准 ASCII 码,给国标码的每个字节加 80H,形成的编码称为机内码,简称内码,是汉字在机器中实际的存储代码GB2312-80 标准的内码范围是 0xA1A1~0xFEFE 。
1.3.2、GBK 编码标准
《汉字内码扩展规范》(GBK) 于1995年制定,兼容GB2312、GB13000-1、BIG5 编码中的所有汉字,使用双字节编码,编码空间为 0x8140~0xFEFE,共有 23940 个码位,其中 GBK1 区和 GBK2 区也是 GB2312 的编码范围。收录了 21003 个汉字。GBK向下与 GB 2312 编码兼容,向上支持 ISO 10646.1国际标准,是前者向后者过渡过程中的一个承上启下的产物。ISO 10646 是国际标准化组织ISO 公布的一个编码标准,即 Universal Multilpe-Octet Coded Character Set(简称UCS),大陆译为《通用多八位编码字符集》,台湾译为《广用多八位元编码字元集》,它与 Unicode 组织的Unicode编码完全兼容。ISO 10646.1 是该标准的第一部分《体系结构与基本多文种平面》。我国 1993 年以 GB 13000.1 国家标准的形式予以认可(即 GB 13000.1 等同于 ISO 10646.1)。
1.3.3、GB18030编码标准
国家标准GB18030-2000《信息交换用汉字编码字符集基本集的补充》是我国继GB2312-1980和GB13000-1993之后最重要的汉字编码标准,是我国计算机系统必须遵循的基础性标准之一。GB18030-2000编码标准是由信息产业部和国家质量技术监督局在2000年 3月17日联合发布的,并且将作为一项国家标准在2001年的1月正式强制执行。GB18030-2005《信息技术中文编码字符集》是我国制订的以汉字为主并包含多种我国少数民族文字(如藏、蒙古、傣、彝、朝鲜、维吾尔文等)的超大型中文编码字符集强制性标准,其中收入汉字70000余个。
1.4、URL编码
URL编码是浏览器发送数据给服务器时使用的编码,通常附加在URL的参数部分。
例如 https://www.baidu.com/s?wd=中文 URL编码后变成https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87
之所以需要URL编码,是因为处于兼容性考虑,很多服务器只识别ASCII字符。但是如果URL中包含中文、日文这些非ASCII字符怎么处理?URL编码规则:
如果字符是A~Z, a~z,0~9以及-,_,. , * ,则保持不变。
如果是其他字符,先转换为UTF-8编码,然后对每个字节以 %xx 表示。
例如:字符中的UTF-8编码是0xe4b8ad,因此,它的URL编码是%E4%B8%AD。URL编码总是大写
1.4.1 编码
示例
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
String encoded = URLEncoder.encode("中文!", StandardCharsets.UTF_8.name());
System.out.println(encoded);
}
}
1.4.2 解码
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", StandardCharsets.UTF_8.name());
System.out.println(decoded);
}
}
需要注意的是,URL编码是编码算法,不是加密算法。URL编码目的是把任意文本数据便于浏览器和服务器处理。
1.5、Base 64
Base 64 编码是对二进制数据进行编码,表示成文本格式。
Base 64编码可以把任意长度的二进制数据变为纯文本,且包含A~Z,a~z,0~9,+, / , =这些字符。
它的原理是把3字节的二进制数据按6bit一组,用4个int整数表示,然后查表,把int整数用索引对应到字符,得到编码后的字符串。
举个例子:3个byte数据分别是e4、b8、ad,按6bit分组得到39、0b、22和2d:
因为6位整数的范围总是0~63,所以,能用64个字符表示:字符A~Z对应索引0~25,字符a~z对应索引26~51,字符0~9对应索引52~61,最后两个索引62、63分别用字符+和/表示
示例
import java.util.Base64;
public class Main {
public static void main(String[] args) {
// Original byte[]
byte[] bytes = "hello world".getBytes();
// Base64 Encoded
String encoded = Base64.getEncoder().encodeToString(bytes);
System.out.println("encoded = " + encoded);
// Base64 Decoded
byte[] decoded = Base64.getDecoder().decode(encoded);
// Verify original content
System.out.println("decoded = " + new String(decoded));
}
}
控制台显示
encoded = aGVsbG8gd29ybGQ=
decoded = hello world
如果输入的byte[] 数组长度不是 3 的整数倍怎么办,在这种情况下,需要对输入的末尾补一个或者两个0x00,编码后,在结尾加一个 = 表示补充了1个0x00,加两个 = 表示补充了2个0x00,解码的时候,去掉末尾补充的一个或者两个0x00 即可。
实际上,因为编码后的长度加上 = 总是 4 的倍数,所以即使不加也可以输出原始输入的byte[] 。 Base64编码的时候可以用withoutPadding()去掉=,解码出来的结果是一样的:
import java.util.Arrays;
import java.util.Base64;
public class Main {
public static void main(String[] args) {
byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad, 0x21 };
String b64encoded = Base64.getEncoder().encodeToString(input);
String b64encoded2 = Base64.getEncoder().withoutPadding().encodeToString(input);
System.out.println(b64encoded);
System.out.println(b64encoded2);
byte[] output = Base64.getDecoder().decode(b64encoded2);
System.out.println(Arrays.toString(output));
}
}
二、Hash 算法
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
特点:
- 相同的输入一定得到相同的输出。
- 不同的输入大概率得到不同的输出。
哈希算法的目的就是为了验证原始数据是否被篡改。
Java 字符串的 hashCode() 就是一个哈希算法,他的输入就是任意字符串,输出就是一个 4 个字节 int 整数。
public class Main {
public static void main(String[] args) {
System.out.println("hello".hashCode());// 99162322
System.out.println("hello world".hashCode());//1794106052
}
}
两个相同字符串永远计算出相同的 hashCode,否则基于hashCode 定位的HashMap 就无法正常工作。这也是我们定义一个类时,覆写 equals() 方法时必须正确覆写 hashCode() 方法。
2.1、 哈希碰撞
哈希碰撞是指,两个不同的输入得到了相同的输出:
public class Main {
public static void main(String[] args) {
System.out.println("AaAaAa".hashCode());// 1952508096
System.out.println("BBAaBB".hashCode());// 1952508096
}
}
碰撞能不能避免,不能。string 的 hashCode() 输出是 4 个字节整数,最多只有4294967296种输出,但是输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无线的输入集合映射到一个有限的输出集合,会产生碰撞。
碰撞不可怕,担心是不是碰撞,而是发生碰撞的概率,因为碰撞概率,因为碰撞概率的高低关系到哈希算法的安全性,一个安全的哈希算法必须满足:
- 碰撞的概率低
- 不能猜测输出
不能猜测输出指的是,输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)。
假设
hashA("java001") = "123456"
hashA("java002") = "123457"
hashA("java003") = "123458"
那么很容易从输出123459反推输入,这种哈希算法就不安全。安全的哈希算法从输出是看不出任何规律的:
hashB("java001") = "123456"
hashB("java002") = "580271"
hashB("java003") = ???
2.2、常用哈希算法
算法 | 输出长度(位) | 输出长度(字节) |
MD5 | 128 bits | 16 bytes |
SHA-1 | 160 bits | 20 bytes |
RipeMD-160 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
SHA-512 | 512 bits | 64 bytes |
根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
2.3、Java标准库提供的哈希算法
Java 标准库提供了常用的哈希算法,并且有一套统一的接口。
MD5示例:
import java.math.BigInteger;
import java.security.MessageDigest;
public class Main {
public static void main(String[] args) throws Exception {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest(); // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
System.out.println(new BigInteger(1, result).toString(16));
}
}
使用 MessageDigest 时,首先根据哈希算法获取一个 MessageDigest 实例,然后,反复调用 update(byte[]) 输入数据。当输入结束后,调用 digest() 方法获取 byte[] 数组表示的摘要,最后,把它转换为十六进制的字符串。
2.4、哈希算法用途
相同的输入永远得到相同的输出,因此,如果输入被修改了,得到输出就不会相同。
在网站上下载软件的时候,经常看到下载页面显示的哈希:
如何判断下载本地的软件是原始的,未经篡改的文件,只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
2.5、哈希算法存储用户口令
哈希算法的另外一个重要的用途是存储用户口令。如果直接将用户的原始口令放到数据库中,会产生极大的安全风险:
- 数据库管理员能看到明文口令
- 数据库一旦泄露,黑客即可获取用户明文口令
不存储用户的原始口令,那如何对用户进行认证。方法是存储的用户口令的哈希,例如 MD5。
在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。
因此,数据库存储用户名和口令内容应该如下:
username | password |
bob | f30aa7a662c728b7407c54ae6bfd27d1 |
alice | 25d55ad283aa400af464c76d713c07ad |
数据库管理员看不到用户名的原始口令,即使数据库泄露,黑客也无法拿到用户的原始口令。想要破解,唯有暴力破解。
当然这种也不能防止彩虹表攻击。
彩虹表:彩虹表可以简单理解,有一个常用MD5口令库,黑客通过比较MD5口令库查到明文口令。
彩虹表库:
常用口令 | MD5 |
hello123 | f30aa7a662c728b7407c54ae6bfd27d1 |
12345678 | 25d55ad283aa400af464c76d713c07ad |
passw0rd | bed128365216c019988915ed3add75fb |
19700101 | 570da6d5277a646f6552b8832012f5dc |
这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令:
bob的MD5:f30aa7a662c728b7407c54ae6bfd27d1,原始口令:hello123;
即使用户使用了常用口令,我们也可以采取措施来抵御彩虹表攻击,方法是对每个口令额外添加随机数,这个方法称之为加盐(salt):
digest = md5(salt+inputPassword)
经过加盐处理的数据库表,内容如下:
username | salt | password |
bob | H1r0a | a5022319ff4c56955e22a74abcc2c210 |
alice | 7$p2w | e5de688c99e961ed6e560b972dab8b6a |
tim | z5Sk9 | 1eee304b92dc0d105904e7ab58fd2f64 |
加盐的目的在于使黑客的彩虹表失效,即使用户使用常用口令,也无法从MD5反推原始口令。
2.6、SHA-1
SHA-1 也是一种哈希算法,他的输出是160 bits,即20字节。SHA-1是由美国国家安全局开发的,SHA算法实际上是一个系列,包括SHA-0(已废弃)、SHA-1、SHA-256、SHA-512等。
在Java中使用SHA-1,和MD5完全一样,只需要把算法名称改为"SHA-1":
import java.math.BigInteger;
import java.security.MessageDigest;
public class Main {
public static void main(String[] args) throws Exception {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest(); // 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2
System.out.println(new BigInteger(1, result).toString(16));
}
}
三、Hmac算法
Hmac 算法就是一种基于秘钥消息的认证算法。它的全称是 Hash-based Message Authentication Code ,是一种更安全的消息摘要算法。
Hmac 算法总是和某种哈希算法合起来使用。例如 我们使用MD5算法,对应的就是HmacMD5算法,它相当于“加盐”的MD5:
HmacMD5 ≈ md5(secure_random_key, input)
HmacMD5 可以看作有一个安全的key的MD5。使用HamcMD5而不是用MD5加盐。
Hmac有如下优点:
- HmacMD5 使用 key长度64字节,更安全
- Hmac是标准算法,同样使用与SHA-1等其他哈希算法
- Hmac输出和原有的哈希算法长度一致
Hmac本质上就是把key 混入摘要的算法。验证此哈希时,除了原始的输入数据,还提供key。
为了保证安全,不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key。
下面是使用HmacMD5的代码:
import java.math.BigInteger;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class Main {
public static void main(String[] args) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
// 打印随机生成的key:
byte[] skey = key.getEncoded();
System.out.println(new BigInteger(skey).toString(16));
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(new BigInteger(1, result).toString(16));
}
}
和MD5相比,使用HmacMD5的步骤是:
1、通过名称HmacMD5获取 KeyGenerator
2、通过KeyGenerator 创建 SecretKey 实例
3、通过名称 HmacMD5 获取 Mac实例
4、用SecreKey 初始化Mac
5、对Mac 实例返回调用updata(byte[])输入数据
6、调用Mac实例doFinal()获取最终哈希值
可以用Hmac算法取代原有的自定义的加盐算法,因此,存储用户名和口令的数据库结构如下:
username | secret_key (64 bytes) | password |
bob | a8c06e05f92e...5e16 | 7e0387872a57c85ef6dddbaa12f376de |
alice | e6a343693985...f4be | c1f929ac2552642b302e739bc0cdbaac |
tim | f27a973dfdc0...6003 | af57651c3a8a73303515804d4af43790 |
有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?
示例
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class Main {
public static void main(String[] args) throws Exception {
// 获取随机生成的key(盐)
String secret_key = getEncodedStr();
BigInteger bigInteger = new BigInteger(secret_key,16);
byte[] hkey = bigInteger.toByteArray();
SecretKey key = new SecretKeySpec(hkey, "HmacMD5");
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(new BigInteger(1, result).toString(16));
}
private static String getEncodedStr() throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
// 打印随机生成的key:
byte[] skey = key.getEncoded();
String k = new BigInteger(skey).toString(16);
System.out.println(k);
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(new BigInteger(1, result).toString(16));
return k;
}
}
四、对称加密算法
对称加密算法就是传统的用一个密码进行加密和解密。例如:常用的WinZIP和WinRAR对压缩包的加密和解密,就是使用对称加密算法:
从程序角度看,加密就是一个方法,它接受密码和明文,然后输出密文:
String secret = encrypt(key, message);
解密则相反,它接受密码金额密文,然后输出明文:
String plain = decrypt(key, secret);
4.1对称加密算法
算法 | 密钥长度 | 工作模式 | 填充模式 |
DES | 56/64 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/... |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA | 128 | ECB | PKCS5Padding/PKCS7Padding/... |
秘钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选择。Java 标准库提供的算法实现并不包括所有工作模式和填充模式。
DES算法由于秘钥过短,可以在短时间内破解,目前不是很安全。
4.2 AES ECB工作模式加密
AES算法是目前应用最广泛的对称加密算法。
示例
import java.security.*;
import java.util.Base64;
import javax.crypto.*;
import javax.crypto.spec.*;
public class Main {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 128位密钥 = 16 bytes Key:
byte[] key = "1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(input);
}
}
Java标准库提供的对称加密接口非常简单,如下步骤:
1、根据算法名称/工作模式/填充模式获取 Cipher示例
2、根据算法名称初始化一个SecretKey实例,秘钥必须指定长度
3、使用SecretKey初始化 cipher 示例,并设置加密与解密模式
4、传入明文或密文,获取密文或明文
4.3 AES CCB工作模式加密
ECB模式是最简单的AES加密模式,只需要一个固定长度的秘钥,固定的明文会生成固定的密文,这种一对一的加密方式会导致安全性降低,更好的方式通过CBC模式,它需要一个随机数作为IV参数,这样对于同一份明文,每次生成的密文都不同。
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Main {
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world!";
System.out.println("Message: " + message);
// 256位密钥 = 32 bytes Key:
byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// CBC模式需要生成一个16 bytes的initialization vector:
SecureRandom sr = SecureRandom.getInstanceStrong();
byte[] iv = sr.generateSeed(16);
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);
byte[] data = cipher.doFinal(input);
// IV不需要保密,把IV和密文一起返回:
return join(iv, data);
}
// 解密:
public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {
// 把input分割成IV和密文:
byte[] iv = new byte[16];
byte[] data = new byte[input.length - 16];
System.arraycopy(input, 0, iv, 0, 16);
System.arraycopy(input, 16, data, 0, data.length);
// 解密:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
return cipher.doFinal(data);
}
public static byte[] join(byte[] bs1, byte[] bs2) {
byte[] r = new byte[bs1.length + bs2.length];
System.arraycopy(bs1, 0, r, 0, bs1.length);
System.arraycopy(bs2, 0, r, bs1.length, bs2.length);
return r;
}
}
在CBC模式下,炫耀一个随机生成的16字节IV参数,必须使用 SecureRandom 生成。因为多了一个IvParameterSpec实例,因此,初始化方法需要调用cipher的一个重置方法并传入IvParameterSpec。
可以看到,每次生成的IV不同,密文也不同。
五、口令加密算法
对于AES加密,秘钥长度固定的128/192/256位,而不是用WinZip/WinRAR那样,随便输入几位都可以。
这个是因为对称加密算法决定了口令必须是固定的长度,然后对明文分块加密。又因为安全需求,口令长度往往都是128位以上,至少16个字符。
但是平时使用加密软件,输入6位、8位都可以。实际上用户输入的口令并不能直接作为AES的秘钥进行加密(除非长度恰好是128/192/256位),并且用户输入的口令一般都是有规律,安全性远远不如随机数产生的随机口令。因此,用户输入的口令,通常需要使用PBE算法,采用随机数杂凑计算出真正的密钥,再进行加密。
PBE就是Password Based Encryption的缩写,它的作用如下:
key = generate(userPassword, secureRandomPassword);
PEB的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的秘钥。以AES密钥为例,我们让用户输入一个口令,然后生成一个随机数,通过PBE算法计算出真正的AES口令,再进行加密,代码如下:
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class Main {
public static void main(String[] args) throws Exception {
// 把BouncyCastle作为Provider添加到java.security:
Security.addProvider(new BouncyCastleProvider());
// 原文:
String message = "Hello, world!";
// 加密口令:
String password = "hello12345";
// 16 bytes随机Salt:
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);
System.out.printf("salt: %032x\n", new BigInteger(1, salt));
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(password, salt, data);
System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(password, salt, encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.DECRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
}
使用PBE时,还需要引入BouncyCastle,并指定算法是PBEwithSHA1and128bitAES-CBC-BC。实际上真正的AES秘钥是调用Cipher的init()方法时同时传入SecretKey和PBEParameterSpec实现的。在创建PBEParameterSpec的时候,还指定了循环次数1000,循环次数越多,暴力破解需要的计算量就越大。
把salt和循环次数固定,就得到了一个通用的“口令”加密软件。把随机生成的salt存储在U盘,就得到了一个“口令”加USB Key的加密软件,它的好处在于,即使用户使用了一个非常弱的口令,没有USB Key仍然无法解密,因为USB Key存储的随机数密钥安全性非常高。
六、秘钥交换算法
例子:在现实世界中,小明要向路人甲发送一个加密文件,他可以先生成一个AES密钥,对文件进行加密,然后把加密文件发送给对方。因为对方要解密,就必须需要小明生成的密钥。
现在问题来了:如何传递密钥?
密钥交换算法即DH算法:Diffie-Hellman。
DH算法解决了密钥在双方不直接传递密钥的情况下完成密钥交换,这个神奇的交换原理完全由数学理论支持。
DH算法交换密钥的步骤。假设甲乙双方需要传递密钥,他们之间可以这么做:
1、甲首选选择一个素数p,例如97,底数g是p的一个原根,例如5,随机数a,例如123,然后计算A=g^a mod p,结果是34,然后,甲发送p=97,g=5,A=34给乙;
2、乙方收到后,也选择一个随机数b,例如,456,然后计算B = g^b mod p,结果是75,乙再同时计算s = A^b mod p,结果是22;
3、乙把计算的B=75发给甲,甲计算s = B^a mod p,计算结果与乙算出的结果一样,都是22。
所以最终双方协商出的密钥s是22。注意到这个密钥s并没有在网络上传输。而通过网络传输的p,g,A和B是无法推算出s的,因为实际算法选择的素数是非常大的。
所以,更确切地说,DH算法是一个密钥协商算法,双方最终协商出一个共同的密钥,而这个密钥不会通过网络传输。
如果我们把a看成甲的私钥,A看成甲的公钥,b看成乙的私钥,B看成乙的公钥,DH算法的本质就是双方各自生成自己的私钥和公钥,私钥仅对自己可见,然后交换公钥,并根据自己的私钥和对方的公钥,生成最终的密钥secretKey,DH算法通过数学定律保证了双方各自计算出的secretKey是相同的。
使用Java实现DH算法的代码如下:
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.KeyAgreement;
public class Main {
public static void main(String[] args) {
// Bob和Alice:
Person bob = new Person("Bob");
Person alice = new Person("Alice");
// 各自生成KeyPair:
bob.generateKeyPair();
alice.generateKeyPair();
// 双方交换各自的PublicKey:
// Bob根据Alice的PublicKey生成自己的本地密钥:
bob.generateSecretKey(alice.publicKey.getEncoded());
// Alice根据Bob的PublicKey生成自己的本地密钥:
alice.generateSecretKey(bob.publicKey.getEncoded());
// 检查双方的本地密钥是否相同:
bob.printKeys();
alice.printKeys();
// 双方的SecretKey相同,后续通信将使用SecretKey作为密钥进行AES加解密...
}
}
class Person {
public final String name;
public PublicKey publicKey;
private PrivateKey privateKey;
private byte[] secretKey;
public Person(String name) {
this.name = name;
}
// 生成本地KeyPair:
public void generateKeyPair() {
try {
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DH");
kpGen.initialize(512);
KeyPair kp = kpGen.generateKeyPair();
this.privateKey = kp.getPrivate();
this.publicKey = kp.getPublic();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void generateSecretKey(byte[] receivedPubKeyBytes) {
try {
// 从byte[]恢复PublicKey:
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(receivedPubKeyBytes);
KeyFactory kf = KeyFactory.getInstance("DH");
PublicKey receivedPublicKey = kf.generatePublic(keySpec);
// 生成本地密钥:
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(this.privateKey); // 自己的PrivateKey
keyAgreement.doPhase(receivedPublicKey, true); // 对方的PublicKey
// 生成SecretKey密钥:
this.secretKey = keyAgreement.generateSecret();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public void printKeys() {
System.out.printf("Name: %s\n", this.name);
System.out.printf("Private key: %x\n", new BigInteger(1, this.privateKey.getEncoded()));
System.out.printf("Public key: %x\n", new BigInteger(1, this.publicKey.getEncoded()));
System.out.printf("Secret key: %x\n", new BigInteger(1, this.secretKey));
}
}
但是DH算法并未解决中间人攻击,即甲乙双方并不能确保与自己通信的是否真的是对方。消除中间人攻击需要其他方法。
DH算法是一种密钥交换协议,通信双方通过不安全的信道协商密钥,然后进行对称加密传输。
七、非对称加密算法
从DH算法可以看到,公钥-私钥组成的秘钥对是非常有用的机密方式,因为公钥是公开的,而私钥是完全保密,由此奠定了非对称加密的基础。
非对称加密就是加密和解密使用的不是相同的秘钥:只有同一个公钥-私钥才能正常加解密。
例如:如果小强需要加密一个文件发送给小红,小强首先向小红索取她的公钥,然后,小强用小红的公钥加密,把加密文件发送给小红,此文件只能小红的私钥解开,因为小红的私钥在她自己手里,所以,除了小红,没有任何人能解开此文件。
非对称加密典型加密算法是RSA算法。RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA就是他们三人姓氏开头字母拼在一起组成的。
非对称加密相比对称加密的显著优点在于,对称加密需要协商秘钥,而非对称加密可以安全地公开各自的公钥,在N个人之见通信的时候,使用非对称加密需要N个秘钥。而使用对称加密则需要 N*(N-1)/2个秘钥。
在实际应用的时候,非对称加密总是和对称加密一起使用。例如小强需要给小红需要传输加密文件,他俩首先交换了各自的公钥,然后:
- 小强生成随机的AES口令,然后用小红的公钥通过RSA加密这个口令,并发送给小红。
- 小红用自己的RSA私钥解密得到AES口令。
- 双方使用这个共享的AES口令用AES加密通信。
Java 标准库提供了RSA算法的实现:
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import javax.crypto.Cipher;
public class Main {
public static void main(String[] args) throws Exception {
// 明文:
byte[] plain = "Hello, encrypt use RSA".getBytes("UTF-8");
// 创建公钥/私钥对:
Person alice = new Person("Alice");
// 用Alice的公钥加密:
byte[] pk = alice.getPublicKey();
System.out.println(String.format("public key: %x", new BigInteger(1, pk)));
byte[] encrypted = alice.encrypt(plain);
System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));
// 用Alice的私钥解密:
byte[] sk = alice.getPrivateKey();
System.out.println(String.format("private key: %x", new BigInteger(1, sk)));
byte[] decrypted = alice.decrypt(encrypted);
System.out.println(new String(decrypted, "UTF-8"));
}
}
class Person {
String name;
// 私钥:
PrivateKey sk;
// 公钥:
PublicKey pk;
public Person(String name) throws GeneralSecurityException {
this.name = name;
// 生成公钥/私钥对:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
// 把私钥导出为字节
public byte[] getPrivateKey() {
return this.sk.getEncoded();
}
// 把公钥导出为字节
public byte[] getPublicKey() {
return this.pk.getEncoded();
}
// 用公钥加密:
public byte[] encrypt(byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, this.pk);
return cipher.doFinal(message);
}
// 用私钥解密:
public byte[] decrypt(byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, this.sk);
return cipher.doFinal(input);
}
}
RSA的公钥和私钥都可以通过getEncoded() 方法获得以 byte[] 表示二进制数据,并根据需要保存到文件中。要从byte[] 数组恢复公钥或私钥,可以这样:
byte[] pkData = ...
byte[] skData = ...
KeyFactory kf = KeyFactory.getInstance("RSA");
// 恢复公钥:
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pkData);
PublicKey pk = kf.generatePublic(pkSpec);
// 恢复私钥:
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(skData);
PrivateKey sk = kf.generatePrivate(skSpec);
以RSA算法为例,它的秘钥256/512/2048/4096等不同的长度。长度越长,密码强大越长,当然计算速度也是越慢。
修改待加密的byte[]数据的大小,可以发现,使用512bit的RSA加密时,明文长度不能超过53字节,使用1024bit的RSA加密时,明文长度不能超过117字节,这也是为什么使用RSA的时候,总是配合AES一起使用,即用AES加密任意长度的明文,用RSA加密AES口令。
非对称加密就是加密和解密使用的不是相同的密钥,只有同一个公钥-私钥对才能正常加解密;
只使用非对称加密算法不能防止中间人攻击。
八、签名算法
使用非对称加密算法的时候,对于一个公钥-私钥对,通常是用公钥加密,私钥解密。
如果使用私钥加密,公钥解密是否可行,实际上是完全可以的。
私钥是保密的,而公钥是公开的。用私钥加密,相当于所有人都可以用公钥解密。加密的意义在那?
例如小强用自己的私钥加密了一条消息,比如“小强喜欢小红”,然后他公开消息,由于任何人都可以用小强的公钥解密,从而使得任何人都可以确认 “小强喜欢小红”这条消息是小强发的,其他人不嫩伪造这条消息,小强不能抵赖这个条消息不是自己的写的。
因此,私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只能用私钥持有者的公钥进行解密验证。使用数字签名的目的是为了确认某个信息确实是由某个发送方发送的,任何人都不可以伪造消息,并且,发送方不能抵赖。
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名,即:
String signature = encrypt(privateKey, sha256(message))
对签名进行验证实际上是用公钥解密:
String hash = decrypt(publicKey, signature)
然后把解密后的哈希与原始消息的哈希进行对比。
因为用户是使用自己的私钥进行签名,所以,私钥就相当于用户身份。而公钥用来给外部验证用户身份。
常用数字签名算法有:
- MD5withRSA
- SHA1withRSA
- SHA256withRSA
实际上就是指定某种哈希算法进行RSA签名算法。
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
public class Main {
public static void main(String[] args) throws GeneralSecurityException {
// 生成RSA公钥/私钥:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
PrivateKey sk = kp.getPrivate();
PublicKey pk = kp.getPublic();
// 待签名的消息:
byte[] message = "Hello, I am Bob!".getBytes(StandardCharsets.UTF_8);
// 用私钥签名:
Signature s = Signature.getInstance("SHA1withRSA");
s.initSign(sk);
s.update(message);
byte[] signed = s.sign();
System.out.println(String.format("signature: %x", new BigInteger(1, signed)));
// 用公钥验证:
Signature v = Signature.getInstance("SHA1withRSA");
v.initVerify(pk);
v.update(message);
boolean valid = v.verify(signed);
System.out.println("valid? " + valid);
}
}
使用其他公钥,或者验证签名的时候修改原始信息,都无法验证成功。
DSA签名
除了RSA可以签名外,还可以使用DSA算法进行签名。DSA是Digital Signature Algorithm的缩写,它使用ElGamal数字签名算法。
DSA只能配合SHA使用,常用的算法有:
- SHA1withDSA
- SHA256withDSA
- SHA512withDSA
和RSA数字签名相比,DSA的优点是更快
ECDSA签名
圆曲线签名算法ECDSA:Elliptic Curve Digital Signature Algorithm也是一种常用的签名算法,它的特点是可以从私钥推出公钥。比特币的签名算法就采用了ECDSA算法,使用标准椭圆曲线secp256k1。BouncyCastle提供了ECDSA的完整实现
数字签名就是用发送方的私钥对原始数据进行签名,只有用发送方公钥才能通过签名验证。
数字签名用于:
- 防止伪造;
- 防止抵赖;
- 检测篡改。
常用的数字签名算法包括:MD5withRSA/SHA1withRSA/SHA256withRSA/SHA1withDSA/SHA256withDSA/SHA512withDSA/ECDSA等
九、数字证书
摘要算法用来确认数据没有篡改,非对称加密算法可以对数据进行加解密,签名算法可以确认数据的完整性和抗否认性,把这些算法集合在一起,就是一套完善的标准,就是所谓的数值证书。
数字证书集合了许多种密码学的算法,用于实现数据加解密、身份认证、签名等多种功能的一种安全标准。
数字证书可以防止中间人攻击,因为采用链式签名认证。即通过根证书(Root CA)去签名下一级证书,这样层层签名,直到最终用户证书。而Root CA 证书内置于操作系统中,所以,任何经过CA认证的数字证书都可以对其本身进行校验,确保证书本身不是伪造。
要使用数字证书,首先需要创建证书,正常情况下,一个合法的数字证书需要经过CA签名,这需要认证域名并支付一定的费用。
在Java程序中,数字证书存储在一种Java专用的key store 文件中,JDK提供了一系列命令老创建和管理key store 。开发测试可以使用。例如下面命令创建 key store ,并设定口令 1234546;
keytool -storepass 123456 -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 3650 -alias mycert -keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN"
参数说明:
- keyalg:指定RSA加密算法;
- sigalg:指定SHA1withRSA签名算法;
- validity:指定证书有效期3650天;
- alias:指定证书在程序中引用的名称;
- dname:最重要的CN=www.sample.com指定了Common Name,如果证书用在HTTPS中,这个名称必须与域名完全一致
执行上述命令,JDK会在当前目录创建一个my.keystore文件,并存储创建成功的一个私钥和一个证书,它的别名是mycert。
import java.io.InputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import javax.crypto.Cipher;
public class Main {
public static void main(String[] args) throws Exception {
byte[] message = "Hello, use X.509 cert!".getBytes("UTF-8");
// 读取KeyStore:
KeyStore ks = loadKeyStore("/my.keystore", "123456");
// 读取私钥:
PrivateKey privateKey = (PrivateKey) ks.getKey("mycert", "123456".toCharArray());
// 读取证书:
X509Certificate certificate = (X509Certificate) ks.getCertificate("mycert");
// 加密:
byte[] encrypted = encrypt(certificate, message);
System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));
// 解密:
byte[] decrypted = decrypt(privateKey, encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
// 签名:
byte[] sign = sign(privateKey, certificate, message);
System.out.println(String.format("signature: %x", new BigInteger(1, sign)));
// 验证签名:
boolean verified = verify(certificate, message, sign);
System.out.println("verify: " + verified);
}
static KeyStore loadKeyStore(String keyStoreFile, String password) {
try (InputStream input = Main.class.getResourceAsStream(keyStoreFile)) {
if (input == null) {
throw new RuntimeException("file not found in classpath: " + keyStoreFile);
}
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(input, password.toCharArray());
return ks;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
static byte[] encrypt(X509Certificate certificate, byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(certificate.getPublicKey().getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
return cipher.doFinal(message);
}
static byte[] decrypt(PrivateKey privateKey, byte[] data) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
static byte[] sign(PrivateKey privateKey, X509Certificate certificate, byte[] message)
throws GeneralSecurityException {
Signature signature = Signature.getInstance(certificate.getSigAlgName());
signature.initSign(privateKey);
signature.update(message);
return signature.sign();
}
static boolean verify(X509Certificate certificate, byte[] message, byte[] sig) throws GeneralSecurityException {
Signature signature = Signature.getInstance(certificate.getSigAlgName());
signature.initVerify(certificate);
signature.update(message);
return signature.verify(sig);
}
}
cmd需要用管理员身份执行命令生成数字证书。
在上述代码中,从key store 直接读取了私钥-公钥对,私钥以PrivateKey 示例表示,公钥以X509Certificate 表示,实际上数字证书只包含公钥,因此,读取证书并不需要口令,只有读取私钥才需要。
HTTPS协议为例,浏览器和服务器建立安全连接的步骤如下:
1、浏览器向服务器发起请求,服务器向浏览器发送自己的数字证书。
2、浏览器用操作系统内置的Root CA来验证服务器的证书是否有效,如果有效,就使用该证书加密一个随机的AES口令并发送给服务器。
3、服务器用自己的私钥解密获得AES口令,并在后续通讯中使用AES加密。以上是单向验证流程。