数据安全
数据的安全传输要从以下几方面入手
- 防窃听
- 防篡改
- 防伪造
现代计算机加密技术已经成为一门学科,基于数学理论,很难!这部分可以做了解
URL编码
- 类似于utf8等字符集编码规则
- URL编码规则:
- URL编码是编码算法,为了便于浏览器和服务器处理!不是加密算法
Base64编码
- 把二进制数据用文本表示的编码算法,适用于文本协议,例如电子邮件协议
- 例如将“中”进行Base64编码:
import java.util.Base64;
public class SecBase64 {
public static void main(String[] args) throws Exception {
String original = "Hello\u00ff编码测试";
String b64 = Base64.getEncoder().encodeToString(original.getBytes("UTF-8"));
System.out.println(b64);// 如何要转成适合URL传输的文本,需要使用getUrlEncoder()
String ori = new String(Base64.getDecoder().decode(b64), "UTF-8");
System.out.println(ori);
}
}
- Base64是编码算法,这种算法会降低效率;
MD5加密算法
- 是一种摘要算法,摘要算法也称为哈希算法/Hash/Digest/数字指纹
- 特点:任意长度的数据输入,固定长度的数据输出
- 目的:验证原始数据是否被篡改
- 碰撞:不同的输入得到相同的输出,因为将任意长度映射到固定长度,碰撞是不可避免的
- 衡量Hash的安全性:
- 常见的Digest
- 使用md5加密算法要注意彩虹表攻击
import java.math.BigInteger;
import java.security.MessageDigest;
public class MD5Salt {
public static byte[] toMD5(byte[] input) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");// SHA-1 SHA-256
} catch (Exception e) {
throw new RuntimeException(e);
}
md.update(input);
return md.digest();
}
public static void main(String[] args) throws Exception {
String passwd = "helloworld";
String salt = "Random salt";
byte[] r = MD5Salt.toMD5((salt + passwd).getBytes("UTF-8"));// 只接受字节类型,需要转化
System.out.println(String.format("%032x", new BigInteger(1, r)));// 转成16进制显示
}
}
Bouncy Castle
- 适用于Java的轻量级加密API
- Java加密扩展(JCE)和Java加密体系结构(JCA)的提供程序
- Java安全套接字扩展(JSSE)的提供程序等
- 可以进行动态和静态配置
import java.security.MessageDigest;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
public class Digest {
public static byte[] digest(String hashAlgorithm, byte[] input) {
MessageDigest md;
try {
md = MessageDigest.getInstance(hashAlgorithm);
} catch (Exception e) {
throw new RuntimeException(e);
}
md.update(input);
return md.digest();
}
public static void main(String[] args) throws Exception {
// 把BouncyCastle作为Provider添加到java.security:
Security.addProvider(new BouncyCastleProvider());// 动态导入jar包到工程,这里注册
String s = "Java摘要算法测试";
byte[] input = s.getBytes("UTF-8");
byte[] r1 = digest("MD5", input);
System.out.println(r1.length + ": " + ByteUtils.toHexString(r1));
byte[] r2 = digest("SHA-1", input);
System.out.println(r2.length + ": " + ByteUtils.toHexString(r2));
byte[] r3 = digest("SHA-256", input);
System.out.println(r3.length + ": " + ByteUtils.toHexString(r3));
byte[] r4 = digest("RipeMD160", input); // JDK没有的算法,有BouncyCastle提供
System.out.println(r4.length + ": " + ByteUtils.toHexString(r4));
}
}
Hmac
- 把key混入摘要的算法
- 可以配合MD5、SHA-1等摘要算法,长度和原摘要算法相同
- 可以理解为加入salt的MD5
import java.math.BigInteger;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class Hmac {
public static byte[] hmac(String hmacAlgorithm, SecretKey skey, byte[] input) throws Exception {
Mac mac = Mac.getInstance(hmacAlgorithm);
mac.init(skey);
mac.update(input);
return mac.doFinal();
}
public static void main(String[] args) throws Exception {
// http://docs.oracle.com/javase/6/docs/technotes/guides/security/StandardNames.html#Mac
String algorithm = "HmacSHA1";
// 原始数据:
String data = "helloworld";
// 随机生成一个Key:
KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
SecretKey skey = keyGen.generateKey();
// 打印Key:
byte[] key = skey.getEncoded();
System.out.println(String.format("Key: %0" + (key.length * 2) + "x", new BigInteger(1, key)));
// 用这个Key计算HmacSHA1:
byte[] result = hmac(algorithm, skey, data.getBytes("UTF-8"));
System.out.println(String.format("Hash: %0" + (result.length * 2) + "x", new BigInteger(1, result)));
}
}
对称加密算法
- 加密密钥和解密密钥相同,大部分算法加密解密过程互逆
- AES_CBC算法:
import java.nio.charset.StandardCharsets;
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 AES_CBC_Cipher {
// 指明算法名称/工作模式/填充模式
static final String CIPHER_NAME = "AES/CBC/PKCS5Padding";
// 加密:
public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
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(CIPHER_NAME);
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;
}
public static void main(String[] args) throws Exception {
// 原文:
String message = "Hello, world! encrypted using AES!";
System.out.println("Message: " + message);
// 128位密钥 = 16 bytes Key:
byte[] key = "1234567890abcdef".getBytes("UTF-8");
// 加密:
byte[] data = message.getBytes(StandardCharsets.UTF_8);
byte[] encrypted = encrypt(key, data);
System.out.println("Encrypted data: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(key, encrypted);
System.out.println("Decrypted data: " + new String(decrypted, "UTF-8"));
}
}
- 使用AES256_ECB算法,由于Oracle收到相关法律限制,需要从官网下载jar包替换JDK的policy文件,否则不能进行256及以上长度密钥的加密!
密钥交换算法
- 如何在不安全的信道上安全传送密钥?
- 原理是各自生成公钥私钥,只传递公钥,互相组合得到相同的密钥!
import java.io.IOException;
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 java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
class Person {
public final String name;
public PublicKey publicKey;
private PrivateKey privateKey;
private SecretKey secretKey;
public Person(String name) {
this.name = name;
}
// 生成本地KeyPair:
public void generateKeyPair() {
try {
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DH");// DH算法的KeyPair
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
// 生成AES密钥:
this.secretKey = keyAgreement.generateSecret("AES");
} 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.getEncoded()));
}
// 发送加密消息:
public String sendMessage(String message) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, this.secretKey);
byte[] data = cipher.doFinal(message.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(data);
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException(e);
}
}
// 接收加密消息并解密:
public String receiveMessage(String message) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, this.secretKey);
byte[] data = cipher.doFinal(Base64.getDecoder().decode(message));
return new String(data, "UTF-8");
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException(e);
}
}
}
public class DH {
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加解密:
String msgBobToAlice = bob.sendMessage("Hello, Alice!");
System.out.println("Bob -> Alice: " + msgBobToAlice);
String aliceDecrypted = alice.receiveMessage(msgBobToAlice);
System.out.println("Alice decrypted: " + aliceDecrypted);
}
}
- 当然,有加密就有破解,如上图所示,如果有中间人假冒,还是会被窃取;
非对称加密算法
- 加密和解密使用不同的密钥
- 只有同一个公钥私钥对才能正常加密/解密
- 优点:
RSA签名算法
- 发送方用自己的私钥对消息进行签名
- 接收方用发送方的公钥检验签名是否有效(只有用私钥的签名才能有效)
- 保证了数据在传输过程中没有被修改,防止伪造的发送方
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.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class SecRSASignature {
PrivateKey sk;
PublicKey pk;
public SecRSASignature() throws GeneralSecurityException {// 还是要获得密钥对
// generate key pair:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
public SecRSASignature(byte[] pk, byte[] sk) throws GeneralSecurityException {
// create from bytes:
KeyFactory kf = KeyFactory.getInstance("RSA");
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pk);
this.pk = kf.generatePublic(pkSpec);
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(sk);
this.sk = kf.generatePrivate(skSpec);
}
public byte[] getPrivateKey() {
return this.sk.getEncoded();
}
public byte[] getPublicKey() {
return this.pk.getEncoded();
}
public byte[] sign(byte[] message) throws GeneralSecurityException {
// sign by sk:
Signature signature = Signature.getInstance("SHA1withRSA");// MD5withRSA SHA256withRSA
signature.initSign(this.sk);
signature.update(message);
return signature.sign();
}
public boolean verify(byte[] message, byte[] sign) throws GeneralSecurityException {
// verify by pk:
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(this.pk);
signature.update(message);
return signature.verify(sign);
}
public static void main(String[] args) throws Exception {
byte[] message = "Hello,使用SHA1withRSA算法进行数字签名!".getBytes("UTF-8");
SecRSASignature rsas = new SecRSASignature();
byte[] sign = rsas.sign(message);
System.out.println("sign: " + Base64.getEncoder().encodeToString(sign));
boolean verified = rsas.verify(message, sign);
System.out.println("verify: " + verified);
// 用另一个公钥验证:
boolean verified2 = new SecRSASignature().verify(message, sign);
System.out.println("verify with another public key: " + verified2);
// 修改原始信息:
message[0] = 100;
boolean verified3 = rsas.verify(message, sign);
System.out.println("verify changed message: " + verified3);
}
}
- 类似的,还有DSA签名算法
数字证书
- 摘要算法保证了数据没被篡改(平时也可加盐作为数据加密)
- 非对称加密算法实现了对数据的加密解密
- 签名算法确保了数据安全传输(完整性、抗否认性)
- 数字证书就是集合了多种密码学算法,用于实现数据加解密、身份认证、签名等多种功能的网络安全标准
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.Base64;
import javax.crypto.Cipher;
public class X509 {
private final PrivateKey privateKey;
public final X509Certificate certificate;
public X509(KeyStore ks, String certName, String password) {
try {
this.privateKey = (PrivateKey) ks.getKey(certName, password.toCharArray());
this.certificate = (X509Certificate) ks.getCertificate(certName);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public byte[] encrypt(byte[] message) {
try {
Cipher cipher = Cipher.getInstance(this.privateKey.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, this.privateKey);
return cipher.doFinal(message);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public byte[] decrypt(byte[] data) {
try {
PublicKey publicKey = this.certificate.getPublicKey();
Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return cipher.doFinal(data);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public byte[] sign(byte[] message) {
try {
Signature signature = Signature.getInstance(this.certificate.getSigAlgName());
signature.initSign(this.privateKey);
signature.update(message);
return signature.sign();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
public boolean verify(byte[] message, byte[] sig) {
try {
Signature signature = Signature.getInstance(this.certificate.getSigAlgName());
signature.initVerify(this.certificate);
signature.update(message);
return signature.verify(sig);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
static KeyStore loadKeyStore(String keyStoreFile, String password) {
try (InputStream input = new BufferedInputStream(new FileInputStream(keyStoreFile))) {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(input, password.toCharArray());
return ks;
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
byte[] message = "Hello,使用X.509证书进行加密和签名!".getBytes("UTF-8");
// 读取KeyStore:
KeyStore ks = loadKeyStore("my.keystore", "456789");// 根据命令行命令生成证书,证书密码
// 读取证书:
X509 x509 = new X509(ks, "mycert", "123456");
// 加密:
byte[] encrypted = x509.encrypt(message);
System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = x509.decrypt(encrypted);
System.out.println("decrypted: " + new String(decrypted, "UTF-8"));
// 签名:
byte[] sign = x509.sign(message);
System.out.println("sign: " + Base64.getEncoder().encodeToString(sign));
// 验证签名:
boolean verified = x509.verify(message, sign);
System.out.println("verify: " + verified);
}
}
- 生成证书命令:(命令行下项目目录执行)
// 如果要应用于网站,域名需保证一致
keytool -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 36500 -alias mycert -keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN" -keypass 123456 -storepass 456789
// 查看证书命令
keytool -list -keystore my.keystore -storepass 456789
小结
安全问题是网络通信的重难点,如果能到研究这一步,算是学有所成了吧!