Java笔记12 - 加密和安全

  • 面对威胁做到
    • 防窃听
    • 防篡改
    • 防伪造
  • 编写计算机程序做到:
    • 不要设计山寨的加密算法
    • 不要实现已有的加密算法
    • 不要修改已有的加密算法

编码算法

  • ASCII编码, 127字符
  • 中文使用Unicode编码
  • 更加全面的是UTF-8

URL编码

  • 如果是ASCII编码能表示的, 就不改变
  • 如果不是, 先转成UTF-8进行表示
  • URL编码总是大写
  • URLEncoder: 对任意字符进行编码
  • URLDecoder: 进行解码
  • URL编码是编码, 不是加密算法, 只是方便浏览器和服务器处理

Base64编码

  • 对二进制数据编码, 表示成文本格式

  • 可以把任意长度的二进制表示成纯文本

  • 并且只包含: A-Z, a-z, +, /, =字符

  • 3字节的二进制按照6bit一组, 用4个int整数表示, 整数用索引对照, 得到字符

  • 6位整数的范围0-63

  • A-Z: 0-25

  • a-z: 26-51

  • 0-9: 52-61

  • 62表示+;63表示/

  • =表示一排的0

  • 如果不是3的整倍数, 就在末尾添加, 一个或者两个0x00, 用1个或者2个=表示

  • 编码的长度总是4的倍数, 所以不加=也可以计算出来: 使用withoutPadding()去掉=

  • +, /, =不适合出现在URL中

  • 针对URL的base64: +编程-, '/'变成_

  • 有点: 在哪里都能存

  • 确定: 原来的长度增加了1/3

哈希算法

  • 又称: 摘要算法

  • 对任意一组输入的数据进行计算, 得到一个固定长度的输出摘要

  • 特点:

    • 相同输入一定得到相同输出
    • 不同的输入大概率得到不同的输出
  • 目的: 为了验证数据是否被篡改

  • hashCode(): 哈希算法, 输入任意字符串, 输出是固定的4字节int的整数

  • HashMap()基于hashCode()工作

哈希碰撞

  • 不同的输入得到了相同的输出
  • 碰撞是必然的, 只需要关注碰撞的概率
  • 安全的哈希算法:
    • 碰撞率低
    • 不能猜测输出
  • 常用的哈希算法:
    • MD5 128bits 16byte
    • SHA-1 160bits 20byte
    • RipeMD-160 160bits 20byte
    • SHA-256 25bits 32byte
    • SHA-512 512bits 64byte
    // 创建一个MessageDigest实例
    MessageDigest md = MessageDigest.getInstance("MD5");
    // 反复调用update输入数据
    md.update("Hello".getBytes("UTF-8"));
    md.update("World".getBytes("UTF-8"));
    // `digest()`获取`byte[]`数组表示的摘要
    byte[] result = md.digest();
    // 转换为16进制的字字符串, 得到md5值
    System.out.println(new BigInteger(1, result).toString(16));

哈希算法用途

  • 防止原始文件被篡改
  • 存储用户口令
    • 用户口令使用hash算法之后, 进行存储
  • 彩虹表攻击
    • 破解方法: 存储常用口令, 一次性破解
    • 抵御方法: 对每个口令添加随机数, 也就是加盐

SHA-1

  • 新的hash算法, 和MD5算法使用方法相同
  • MD5因为输入长度较短, 短时间破解是可能的, 不再推荐

BouncyCastle

  • 提供很多哈希算法和加密算法的第三方库, 例如:RipeMD16哈希算法

疑问

.classpath是什么

  • .classpath文件中去除以下代码, 正常运行
    <attributes>
      <attribute name="module" value="true"/>
    </attributes>
  • 否则报错
Error: Unable to initialize main class com.itranswarp.learnjava.Main
Caused by: java.lang.NoClassDefFoundError: org/bouncycastle/jce/provider/BouncyCastleProvider

Hmac算法

  • 加盐的目的是为了防止用户拿到原始口令
  • Hmac: 基于密钥的消息认证码算法
  • 和某种哈希算法配合使用
  • HmacMD5: 带有一个安全的key的MD5, 不用加盐, 本质: 把key混进摘要的算法
    KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
    SecretKey key = keyGen.generateKey();

    // 打印随机生成的key
    byte[] skey = key.getEncoded();
    System.out.println(new BigInteger(1, 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));
  • 具体操作步骤:

    1. 通过Hmac5获取KeyGenerator
    2. 通过KeyGenerator创建一个SecretKey实例
    3. 通过名称HmacMD5获取Mac实例
    4. SecretKey初始化Mac实例
    5. Mac实例反复调用update(byte[])输入数据
    6. 调用Mac实例的doFinal()获取最终的哈希值
  • SecretKey恢复password: 从byte[]数组中恢复

  • 恢复SecretKey的语句就是: new SecretKeySpec(hkey, "HmacMD5")

byte[] hkey = new byte[] {
    106, 70, -110, 125, 39, -20, 52, 56, 85, 9, -19, -72, 52, -53, 52, -45, -6, 119, -63,
    30, 20, -83, -28, 77, 98, 109, -32, -76, 121, -106, 0, -74, -107, -114, -45, 104, -104, -8, 2, 121, 6,
    97, -18, -13, -63, -30, -125, -103, -80, -46, 113, -14, 68, 32, -46, 101, -116, -104, -81, -108, 122,
    89, -106, -109
};
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(Arrays.toString(result));

对称加密算法

  • 加密: 接受密码和明文, 输出密文
  • 解密: 接受密码和密文, 输出明文
  • 算法概览: 名称/长度/工作模式/填充模式
    • DES: 56/65 ECB/CBC/PCBC/CTR/... NoPadding/PKCS5Padding/...
    • AES: 128/192/256 ECB/CEC/PCBC/CTR/... NoPadding/PKCS5Padding/PKCS7Padding/...
    • IDEA 128 ECB PKCS5Padding/PKCS7Padding/...
  • 秘钥长度决定加加密强度
  • 工作模式和填充模式, 对称加密算法的参数和格式选择
  • DES密码长度过短, 并不安全

使用AES加密

public static void main(String[] args) throws Exception {
    // 原文
    String message = "Hello, world";
    System.out.println("Message: " + message);

    // 128位密钥: 16 bytes key
    byte[] key = "1234567890acbdef".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);
  }
  • 开发步骤:
    1. 根据算法名称/工作模式/填充模式获取Cipher实例
    2. 根据算法名称初始化一个SecretKey实例, 秘钥必须是指定长度
    3. 使用SecretKey初始化Cipher实例, 并设置加密或者解密模式
    4. 传入明文或者密文
  • AES模式过于简单, 通常使用CBC模式, 需要一个随机数IV参数, 同一份明文, 每次的密文都不同
// 加密:
  public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    SecretKey keySpec = new SecretKeySpec(key, "AES");

    // CBC模式需要生成一个16bytes的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);
    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");
    SecretKey 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

口令加密算法

  • 用户输入的口令需要使用PBE算法, 采用随机数杂凑计算真正的秘钥, 再进行加密
  • PBE作用: 把用户输入的口令和一个安全随机的口令采用杂凑后计算真正的秘钥
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); // 如果随机生成的salt放到U盘, 就会得到一个"口令"加USB Key的加密软件
    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); // 真正实现秘钥的时候, 同事传入`SecretKey`和`PBEParameterSpec`实现
    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);
  }

密钥交换算法

  • 密钥交换算法: DH算法.
  • 在不安全的信道上安全的传输密钥.
  • 双方交换公钥, 并利用对方的公钥和自己的私钥计算出一样的密钥.
  • 秘钥交换算法, 并未解决中间人攻击
public class Main {
  public static void main(String[] args) throws Exception {
    Person bob = new Person("bob");
    Person alice = new Person("alice");

    bob.generateKeyPair();
    alice.generateKeyPair();

    bob.generateSecretKey(alice.publicKey.getEncoded());
    alice.generateSecretKey(bob.publicKey.getEncoded());

    bob.printKeys();
    alice.printKeys();
  }
}

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) {
      e.printStackTrace();
    }
  }
  
  public void generateSecretKey(byte[] receivedPubKeyBytes) {
    try {
      // 恢复PublicKey;
      X509EncodedKeySpec keySpec = new X509EncodedKeySpec(receivedPubKeyBytes);
      KeyFactory kf = KeyFactory.getInstance("DH");
      PublicKey receivedPubliKey = kf.generatePublic(keySpec);
      // 生成本地密钥;
      KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
      keyAgreement.init(this.privateKey); // 自己的
      keyAgreement.doPhase(receivedPubliKey, true); // 对方的publickey
      this.secretKey = keyAgreement.generateSecret();
    } catch (GeneralSecurityException e) {
      e.printStackTrace();
    }
  }
  
  public void printKeys() {
    System.out.printf("Name: %s\n", this.name);
    System.out.printf("Private Key: %s\n", new BigInteger(1, this.privateKey.getEncoded()));
    System.out.printf("Public Key: %s\n", new BigInteger(1, this.publicKey.getEncoded()));
    System.out.printf("Secret Key: %s\n", new BigInteger(1, this.secretKey));
  }
}

非对称加密算法

  • 加密和解密使用的不是相同的密钥. 只用同一个公钥-私钥才能正常解密.
  • 非对称加密不需要协商密钥, 可以安全的公开各自的公钥.
  • N个人通信:
    • 非对称加密: 每个人管理自己的密钥对, 速度慢
    • 对称加密: 需要N*(N-1)/2个秘钥, 每个人管理N-1个密钥, 速度快.
  • 具体通信:
    • A生成一个AES口令, 用B的公钥加密, 发送给B
    • B用自己的私钥解开, 得到口令
    • 双方用这个口令进行接下来的通信
  • 公钥私钥都可以通过getEncoded()方法获得byte[]表示的二进制数据, 并保存到文件中
  • RSA加密明文, 有长度限制, 故配合AES. AES可以加密任意长度的明文.
  • 非对称加密算法不能防止中间人攻击
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);
  }
}

签名算法

  • 非对称秘钥, 私钥加密, 公钥解密可行, 意义在于: 签名.
  • 可以确保信息是由某个发送方发送, 任何人不能伪造消息, 并且发送方不能抵赖
  • 实际应用时: 针对原始消息的Hash值进行签名
生成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签名: 更快

ECDSA签名:可以从私钥推出公钥

数字证书

  • 数字证书集合:
    • 摘要算法确保数据没有被篡改
    • 非对称加密算法可以对数据进行加解密
    • 签名算法确保数据完整性和抗否认性
  • 链式签名认证, 防止中间人攻击.
  • 开发的时候, 使用自签名证书.
  • 存储在Java专用的key store中, JDK提供一系列命令来创建和管理key store.
  • 主要参数:
    • keyalg: 指定RSA加密算法
    • sigalg: 指定SHA1withRSA签名算法
    • validity: 指定证书有效期3650天
    • alias: 指定证书在程序中引用的名称
    • dname: 最重要的CN=www.sample.com指定了Common Name, 如果证书在HTTPS中, 这个名称必须和域名完全一致
  • keyStore中直接读取私钥-公钥对.
  • 公钥就是证书表示, 读取私钥需要口令
  • 部署WEB服务器, 如Nginx, 需要把私钥导出为PrivateKey格式, 证书导出为X509Certificate格式.
  • HTTPS为例, 浏览器和服务器建立安全连接步骤如下:
    1. 浏览器向服务器发起请求, 服务器向浏览器下发自己的数字证书
    2. 浏览器用操作系统内置的Root CA来验证服务器的整数是否有效.
    3. 如果有效的话, 就使用该证书加密一个随机的AES口令并发送给服务器
    4. 服务器用自己的私钥解密获得AES口令, 并在后续通讯中使用
    5. 如果需要验证客户端, 客户端需要把自己的证书发送给服务器. 例如网银
  • 数字证书存储的是公钥, 以及相关的证书链, 和算法信息.
  • 私钥需要严格保密.
posted @ 2020-05-08 18:21  张润昊  阅读(170)  评论(0编辑  收藏  举报