Java加密学习笔记第一版
Java加密学习笔记第一版
写在前面,今天自己做个leetcode就做了一个hour, 学个加密,半天看不懂,想了好久,觉得自己都没有成套的知识体系,老是在快餐式的学习,没有计划的学习,今天学这个,明天学哪个,技术能力不但没有提高,反而代码能力都有所下降,这样如何准备即将到来的实习。暑假就是数据结构和java,打好基础,做好笔记,不要去想别的东西了,希望你有所沉淀。
1. 编码算法
1.1 必须掌握的编码知识
这是为了解决计算机如何表示一个char这个问题。计算机底层只能保存二进制,所以我们做了一个映射表,把二进制表示的数字去映射不同的字符。一开始用一个字节来表示不同的字符,最高位不使用,所以实际上就可以表示2**7 -1 = 127个字符。这种表示方法就叫做ascii码。如果说,有的国家字符数多于127个咋办呢? 于是各个国家有了不同的表示方式,中国采用gb2312。后来为了统一,颁布了一个固定字节数的编码方式unicode编码,java中采用了这种方式,占2个字节。后来又出现了可变长编码utf-8编码,长度可以变化,更加节省字节。
StandardCharsets.US_ASCII // java中表示ascii码
StandardCharsets.UTF_8 // java中表示utf-8编码
byte[] bytes = new byte[]{1,2,34,5};
String str = new String(bytes, StandardCharsets.UTF_8); // byte数组转String,指定utf-8编码
byte[] bytes = "hell0".getBytes(StandardCharsets.UTF_8); // String转byte[],指定utf-8编码
有些时候,我们在进行编码转换的时候,常常出现乱码,前段时间的爬虫项目,当将字节流转换成字符的时候,老是乱码。有些时候读文件也会乱码。这个时候应该优先设置utf-8编码。有的时候,服务器只支持ascii码,只能用ascii码了。
1.2. 常见的编码算法
URL编码算法
// url编码
// url 需要编码,有些服务器只认识ascii
// 编码规则
/**
* 26个英文小写字母和它的大写字母以及- _ . * 不变
* 如果是其他字符,先转换为UTF-8编码,然后对每个字节以%XX表示
*/
// url编码器
String encoded = URLEncoder.encode("hhf-_.*;!付清", StandardCharsets.UTF_8);
System.out.println(encoded);
// url解码器
String decodeString = URLDecoder.decode(encoded,StandardCharsets.UTF_8);
System.out.println(decodeString);
Base64编码算法
public class Base64Test {
public static void main(String[] args) {
/**
* Base64编码是对二进制数据进行编码,表示成文本格式。
* Base64编码可以把任意长度的二进制数据变为纯文本,
* 且只包含A~Z、a~z、0~9、+、/、=这些字符。
* 它的原理是把3字节的二进制数据按6bit一组,
* 用4个int整数表示,然后查表,把int整数用索引对应到字符,
* 得到编码后的字符串
* 因为6位整数的范围总是0~63,所以,
* 能用64个字符表示:字符A~Z对应索引0~25,
* 字符a~z对应索引26~51,字符0~9对应索引52~61,
* 最后两个索引62、63分别用字符+和/表示
*/
byte[] bytes = new byte[]{1,3,4,3,2};
// 编码
String base64encoded = Base64.getEncoder().encodeToString(bytes);
System.out.println(base64encoded);
// 不是三的倍数,用等号补上,结果用withoutPadding去掉
System.out.println(Base64.getEncoder().withoutPadding().encodeToString(bytes));
// 解码
byte[] output = Base64.getDecoder().decode(base64encoded);
System.out.println(Arrays.toString(output));
// 应用场景
/**
Base64编码的目的是把二进制数据变成文本格式,
这样在很多文本中就可以处理二进制数据。例如,
电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,
就可以用Base64编码,然后以文本的形式传送
*/
// 缺点
/**
* 字节数必须是三的倍数
*/
}
}
2. hash算法
2.1 hash算法介绍
-
哈希算法(Hash)又称摘要算法(Digest),
它的作用是:对任意一组输入数据进行计算,
得到一个固定长度的输出摘要 -
相同的输入一定得到相同的输出; 不同的输入大概率得到不同的输出。-
-
一个安全的哈希算法必须满足:
碰撞概率低;
不能猜测输出。不能通过hash值猜测对应的原来的字符串。也就是说是单向的,相当于单向加密。 -
常见的hash算法
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.2 应用场景
-
数据登录模块设计。密码的存储常常使用hash算法,而不是直接存储明文。我在想要是发生了hash冲突咋办,错误的password也可以登录。这个应该概率极小吧。
-
防止篡改。
-
补充知识:
// 啥叫彩虹表攻击,如何预防 /** * 黑客获得数据库密码表时,如果你的密码表采用的是md5加密,一般来说,我们 * 可以大量穷举密码明文以获得密码明文。但是黑客不会那么笨,他们会预先准备一个密码 * 字典,列出常用的密码和对应的md5的加密值。如果用户使用了常用密码,那么很快就可以查到 * 对应的明文了。毕竟hashMap查找很快的。但是如果用户不使用常用密码,黑客会实现准备一个彩虹表。 * 怎么解决: 加盐存储,就是给每个摘要附上一个随机值 */
彩虹表资料: https://blog.csdn.net/whatday/article/details/88527936
实现一个简单的彩虹表生成器
2.3 demo
public static void main(String[] args) throws Exception {
byte[] bytes = "World".getBytes(StandardCharsets.US_ASCII);
MessageDigest messageDigest = MessageDigest.getInstance("MD5");// 算法名称
messageDigest.update(new byte[]{1, 4, 2, 1});
messageDigest.update(bytes); // 更新摘要
byte[] res = messageDigest.digest();
; // 重置为初始化状态
// 转化为String
BigInteger bigInteger = new BigInteger(1,res);
String str = bigInteger.toString();
}
2.4 外部类库, java核心库没有RipeMD160这个算法
/**
* BouncyCastle
* 首先去官网下载,idea设置好classpath
*/
public static void main(String[] args) throws Exception{
// 注册BouncyCastle:
Security.addProvider(new BouncyCastleProvider());
// 按名称正常调用:
MessageDigest md = MessageDigest.getInstance("RipeMD160");
md.update("HelloWorld".getBytes("UTF-8"));
byte[] result = md.digest();
System.out.println(new BigInteger(1, result).toString(16)); // 16进制的字符串
}
2.5 基于盐值的算法之Hmac
demo
public static void main(String[] args) throws Exception {
// 生成随机盐值
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
Mac mac = Mac.getInstance("HmacMD5");
// 打印随机生成的key:
byte[] skey = key.getEncoded();
mac.init(key);
mac.update("HelloWorld".getBytes(StandardCharsets.UTF_8));
byte[] result = mac.doFinal();
// 验证过程
System.out.println(isCorrectPassWorld("HelloWorld", result, skey));
System.out.println(isCorrectPassWorld("HelloWorls", result, skey));
}
static boolean isCorrectPassWorld(String info, byte[] result , byte[] salt) throws Exception{
Mac mac = Mac.getInstance("HmacMD5");
SecretKey key = new SecretKeySpec(salt, "HmacMD5");
mac.init(key);
mac.update(info.getBytes(StandardCharsets.UTF_8));
return Arrays.equals(mac.doFinal(), result);
// 数组的equals方法是Object的equals方法,数组也是一个对象
// Arrays.equals()比较的是数组的值
}
3. 对称加密
3.1 对称加密介绍
-
用一个密码加密,同一个密码解密。常用的WinZIP和WinRAR对压缩包的加密和解密,就是使用对称加密算法
-
常用对称加密算法
算法 密钥长度bites 工作模式 填充模式 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/...
-
最后注意,DES算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了
AES算法是目前应用最广泛的加密算法,密钥的长度固定 -
demo
public static void main(String[] args) throws GeneralSecurityException { byte[] encrypt = null; byte[] decrypt = null; Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); SecretKey key = new SecretKeySpec("password11111111".getBytes(StandardCharsets.UTF_8), "AES"); SecretKey errorKey = new SecretKeySpec("password12313211".getBytes(StandardCharsets.UTF_8), "AES"); // 加密encrypt cipher.init(Cipher.ENCRYPT_MODE, key); // 加密模式 cipher.update("fsdfsafsdfsads".getBytes(StandardCharsets.UTF_8)); // 16byte encrypt = cipher.doFinal(); // 解密decrypt cipher.init(Cipher.DECRYPT_MODE, errorKey); try { decrypt = cipher.doFinal(encrypt); System.out.println(new String(decrypt, StandardCharsets.UTF_8)); } catch (BadPaddingException e) { System.out.println("code error"); }
3.2 口令加密算法介绍,解决的是任意长度口令的对称加密问题
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);
}
4.非对称加密
4.1 密钥交换算法DH
-
DH算法是一个密钥协商算法,双方最终协商出一个共同的密钥,而这个密钥不会通过网络传输。
-
如果我们把a看成甲的私钥,A看成甲的公钥,b看成乙的私钥,B看成乙的公钥,DH算法的本质就是双方各自生成自己的私钥和公钥,私钥仅对自己可见,然后交换公钥,并根据自己的私钥和对方的公钥,生成最终的密钥secretKey,DH算法通过数学定律保证了双方各自计算出的secretKey是相同的
4.2 非对称加密介绍
非对称加密基于DH算法,是一种加密和解密所需要的密钥不同的算法。每个人维护一个公钥和私钥。加密的时候,主要有两种方式。
-
一是获得接收方的公钥,利用对方的公钥和自己的私钥加密信息。然后发送给对方,对方获得发送方的公钥,和自己的私钥生成密钥解密。这样看来,解密就好像是利用自己的私钥解密,加密利用自己的公钥的加密。
-
第二种是利用自己的私钥加密,然后发送出去。我们知道,这个信息可以用发送方的公钥解密,那么任何人都可以去解密,因为发送方的公钥是公开的。这样可以防止发送方抵赖。
-
demo
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); } } class Tool { static PrivateKey convertToPrivate(byte[] prikeyData) throws InvalidKeySpecException, NoSuchAlgorithmException { KeyFactory kf = KeyFactory.getInstance("RSA"); PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(prikeyData); return kf.generatePrivate(skSpec); } static PublicKey convertToPublic(byte[] pubkeyData) throws InvalidKeySpecException, NoSuchAlgorithmException { KeyFactory kf = KeyFactory.getInstance("RSA"); X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pubkeyData); return kf.generatePublic(pkSpec); } }
4.3数字签名
私钥加密,公钥解密。别人都知道自己的公钥,因此都可以打开,所有人就都知道这个文件是谁的了,不能抵赖。
私钥加密得到的密文实际上就是数字签名,要验证这个签名是否正确,只能用私钥持有者的公钥进行解密验证。
使用数字签名的目的是为了确认某个信息确实是由某个发送方发送的,任何人都不可能伪造消息,并且,发送方也不能抵赖
在实际应用的时候,签名实际上并不是针对原始消息,而是针对原始消息的哈希进行签名
demo
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"); // SHA1hash算法,对hash值进行签名
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);
}
4.4 数字证书
- 非对称加密算法可以对数据进行摘要算法用来确保数据没有被篡改加解密,签名算法可以确保数据完整性和抗否认性,
把这些算法集合到一起,并搞一套完善的标准,这就是数字证书。- java内部可以自己生成证书,供开发的时候使用,上线的时候不要使用。以后遇到了再来补充笔记。
4.5 对称加密与非对称加密优缺点比较
-
与多对个人通信时, 对称加密要管理多个密码,非对称加密只需要管理好自己的私钥和公钥。
-
非对称加密需要复杂的协商过程,所以如果通信频繁,那么效率也不好。但是如果采用对称加密的话,加密口令在网络中传输,也不安全。一般来说,是这样处理:
-
/** 以HTTPS协议为例,浏览器和服务器建立安全连接的步骤如下: 浏览器向服务器发起请求,服务器向浏览器发送自己的数字证书; 浏览器用操作系统内置的Root CA来验证服务器的证书是否有效,如果有效,就使用该证书的公钥加密一个随机的AES口令并发送给服务器;// AES口令传输采用非对称加密 服务器用自己的私钥解密获得AES口令,并在后续通讯中使用AES加密。 // 对称加密 上述流程只是一种最常见的单向验证。如果服务器还要验证客户端,那么客户端也需要把自己的证书发送给服务器验证,这种场景常见于网银等。 */