數據安全
數據的安全傳輸要從以下幾方面入手
- 防竊聽
- 防篡改
- 防僞造
現代計算機加密技術已經成爲一門學科,基於數學理論,很難!這部分可以做了解
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
小結
安全問題是網絡通信的重難點,如果能到研究這一步,算是學有所成了吧!