Một Mã hóa đối xứng
Mã hóa đối xứng và mã hóa bất đối xứng là hai phương pháp mã hóa khác nhau, đóng vai trò quan trọng trong bảo mật dữ liệu và truyền tải thông tin. Dưới đây tôi sẽ lần lượt giới thiệu hai kỹ thuật mã hóa này:
1.1 Mã hóa đối xứng(Symmetric Encryption)
Mã hóa đối xứng là việc mã hóa và giải mã đều sử dụng cùng một khóa. Điều này có nghĩa là cả bên gửi và bên nhận đều phải có khóa này mới thực hiện được các thao tác mã hóa và giải mã.
Các thuật toán mã hóa đối xứng phổ biến:
AES(Tiêu chuẩn Mã hóa Tiên tiến)
DES(Tiêu chuẩn mã hóa dữ liệu)
3DES(thuật toán mã hóa dữ liệu ba lần)
1.2 Đặc điểm của mã hóa đối xứng
Nhìn chung, mã hóa đối xứng có những đặc điểm sau:
Tốc độ khá nhanh:Do mã hóa và giải mã sử dụng cùng một khóa, mã hóa đối xứng thường nhanh hơn mã hóa bất đối xứng.
Quản lý khóa:Thách thức chính của mã hóa đối xứng nằm ở việc phân phối và quản lý khóa. Nếu khóa bị lộ, tính an toàn của mã hóa sẽ bị đe dọa.
Phù hợp với lượng dữ liệu lớn:Do có tốc độ nhanh, mã hóa đối xứng phù hợp để mã hóa lượng lớn dữ liệu.
Từ đây có thể thấy, mã hóa đối xứng có hai ưu điểm chính: thứ nhất là tốc độ tính toán nhanh; thứ hai là phù hợp với lượng dữ liệu lớn.
Tuy nhiên, mã hóa đối xứng có một vấn đề chí mạng là quản lý khóa. Làm thế nào để truyền khóa an toàn từ phía máy chủ tới phía máy khách là một vấn đề!Ngoài ra, khi giao tiếp một-nhiều, quản lý khóa sao cho không bị rò rỉ cũng là một thử thách. Đây là điểm hạn chế của mã hóa đối xứng.

1.3 Ví dụ mã nguồn
Tiếp theo, anh Tùng sẽ minh họa cho mọi người cách mã hóa và giải mã đối xứng bằng mã Java.
Để triển khai mã hóa đối xứng trong Java, thường sử dụng các lớp và giao diện do Kiến trúc Mật mã Java(Java Cryptography Architecture, JCA)cung cấp.
Dưới đây là một ví dụ đơn giản về mã hóa và giải mã đối xứng bằng thuật toán AES (Tiêu chuẩn Mã hóa Nâng cao):
public class SymmetricEncryptionExample { // 生成密钥
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128); // 可以是128, 192或256位
return keyGenerator.generateKey();
}
// 加密方法
public static String encrypt(String data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
// 解密方法
public static String decrypt(String encryptedData, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
public static void main(String[] args) {
try {
// 生成密钥
SecretKey key = generateKey();
// 原始数据
String originalData = "Hello, JavaBoy!";
// 加密
String encryptedData = encrypt(originalData, key);
System.out.println("加密数据: " + encryptedData);
// 解密
String decryptedData = decrypt(encryptedData, key);
System.out.println("解密数据: " + decryptedData);
} catch (Exception e) {
e.printStackTrace();
}
}
}Tạo khóa:sử dụng
KeyGeneratorTạo một khóa AES.Mã hóa:sử dụng
Cipherdùng lớp để mã hóa, chuyển dữ liệu thành byte rồi mã hóa, đồng thời dùng mã hóa Base64 để chuyển thành chuỗi, thuận tiện cho việc lưu trữ hoặc truyền tải.Giải mã:Giải mã chuỗi đã mã hóa trở lại thành byte, sau đó dùng cùng khóa để giải mật.
Điều mọi người cần chú ý trong đoạn mã trên là:
Độ dài khóa (chẳng hạn 128 bit) nên được chọn theo yêu cầu bảo mật.
Đảm bảo khóa được lưu trữ an toàn, không mã hóa cứng khóa trong mã nguồn.
Đối với môi trường sản xuất, nên cân nhắc sử dụng chiến lược quản lý khóa an toàn hơn.
Vì lý do an toàn, chúng ta thường dùng phương án ở trên để tạo khóa. Khóa được tạo theo phương án này có một đặc điểm là sẽ thay đổi mỗi lần hệ thống khởi động lại. Nếu muốn tự kiểm soát việc tạo khóa, bạn có thể tạo khóa theo cách sau:
public static SecretKey generateKeyFromPassword(String password, int keySize) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), "salt".getBytes(), 65536, keySize);
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
}Như vậy có thể kiểm soát khóa bằng các tham số do chính mình truyền vào.
1.4 Phối hợp frontend và backend
Khi dùng thuật toán AES để mã hóa và giải mã giữa các ngôn ngữ(như JavaScript và Java), điều cốt yếu là bảo đảm hai bên dùng cùng khóa, chế độ thuật toán(như CBC, ECB, v.v.), chế độ đệm(như PKCS5Padding, PKCS7Padding, v.v.)và vectơ khởi tạo(IV, nếu dùng chế độ cần IV như CBC).
Trước đây có bạn nói rằng sau khi mã hóa ở frontend, backend luôn không thể giải mã, nên anh Tùng cũng đưa ra đây một ví dụ kết hợp frontend và backend.
Mã hóa ở frontend, giải mã ở backend
Mã nguồn phía giao diện:
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8">
<title>AES加密示例</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
</head>
<body>
<script>
function encryptAES(text, secretKey) {
const key = CryptoJS.enc.Utf8.parse(secretKey);
const iv = CryptoJS.lib.WordArray.random(128 / 8);
// 对于CBC模式,需要IV
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
// 返回加密后的文本和IV(Base64格式),实际使用中可能需要安全地传输这些值
return {
ciphertext: encrypted.toString(),
iv: iv.toString(CryptoJS.enc.Base64)
};
}
const secretKey = 'helloworldhelloworldhelloworld11'; /
/ 确保密钥是32个字符长(256位)
const text = 'Hello, javaboy!';
const result = encryptAES(text, secretKey);
console.log(result);
</script>
</body>
</html>Mã phía máy chủ:
import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;public class AESDecrypt { public static String decryptAES(String encryptedData, String secretKey, String iv) throws Exception { IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv)); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); byte[] decodedValue = Base64.getDecoder().decode(encryptedData); byte[] decryptedValue = cipher.doFinal(decodedValue); return new String(decryptedValue, "UTF-8");
} public static void main(String[] args) { try { //前端加密后的文本
String encryptedText = "PYANpAjMsRnBIEhovtEXQw=="; String secretKey = "helloworldhelloworldhelloworld11"; //前端 IV,要和加密后的文本一起传到后端
String iv = "y/jUHcgSOpOiyNlsfjNUBg=="; String decryptedText = decryptAES(encryptedText, secretKey, iv);
System.out.println("解密文本: " + decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
}Lưu ý rằng khi mã hóa ở phía giao diện sẽ tạo ra một tham số iv, cần truyền tham số này cùng kết quả mã hóa ở phía giao diện đến phía máy chủ.
Mã hóa ở phía máy chủ, giải mã ở phía máy khách
Khi mã hóa AES trong Java và giải mã trong JavaScript, cũng cần bảo đảm cả hai phía sử dụng cùng khóa, chế độ thuật toán (như CBC, ECB, v.v.), chế độ đệm (như PKCS5Padding, PKCS7Padding, v.v.) và cùng vectơ khởi tạo (IV) nếu có áp dụng.
Mã phía máy chủ:
public class AESEncrypt { public static String encryptAES(String plainText, String secretKey, String iv) throws Exception { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); // 对于AES-256
SecretKey secret = new SecretKeySpec(secretKey.getBytes("UTF-8"), "AES"); IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv)); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret, ivParameterSpec); byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8")); return Base64.getEncoder().encodeToString(encrypted);
} public static void main(String[] args) { try { String plainText = "Hello, 江南一点雨!"; String secretKey = "helloworldhelloworldhelloworld11"; // 确保密钥是32个字符长(256位)
String iv = Base64.getEncoder().encodeToString(new byte[16]); // 示例IV,实际应用中应更安全地生成
String encryptedText = encryptAES(plainText, secretKey, iv);
System.out.println("Encrypted text: " + encryptedText);
System.out.println("IV (Base64): " + iv); // 确保将IV发送给解密方
} catch (Exception e) {
e.printStackTrace();
}
}
}Mã nguồn phía giao diện:
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
<script>
function decryptAES(ciphertext, secretKey, iv) { const key = CryptoJS.enc.Utf8.parse(secretKey); const ivParsed = CryptoJS.enc.Base64.parse(iv); const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: CryptoJS.enc.Base64.parse(ciphertext)
},
key,
{ iv: ivParsed, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7
}
); return decrypted.toString(CryptoJS.enc.Utf8);
} const secretKey = 'helloworldhelloworldhelloworld11'; const iv = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 从Java代码获取
const ciphertext = '1lPNVF1injas78KUWeKp5FusEr6f0pGgcrLAg9ELFr8='; // 从Java代码获取
const decryptedText = decryptAES(ciphertext, secretKey, iv); console.log(decryptedText); </script></head><body></body></html>Tùng ca đã tự kiểm thử các đoạn mã mã hóa và giải mã dùng trong tương tác giữa giao diện và máy chủ ở trên, tất cả đều không có vấn đề. Ai có nhu cầu về mặt này nhớ lưu bài viết để tham khảo.
Hai Mã hóa bất đối xứng
2.1 Mã hóa bất đối xứng là gì
Mã hóa bất đối xứng, còn gọi là mã hóa khóa công khai, là phương thức mã hóa sử dụng hai khóa khác nhau(khóa công khai và khóa riêng).
Nhìn chung, mã hóa bất đối xứng có những đặc điểm khác biệt sau:
Khóa công khai và khóa riêng
Tạo khóa công khai và khóa bí mật:Mã hóa bất đối xứng sử dụng một cặp khóa, khóa công khai được công bố và ai cũng có thể truy cập, còn khóa bí mật được giữ kín, chỉ người sở hữu khóa mới có thể truy cập. Hai khóa này được tạo bằng thuật toán toán học, có liên hệ với nhau nhưng không thể suy ra khóa này từ khóa kia.
Tính bảo mật cao
Khó phá giải: Do quan hệ toán học phức tạp giữa khóa công khai và khóa riêng, mã hóa bất đối xứng có độ an toàn khá cao. Kẻ tấn công rất khó suy ra khóa riêng từ khóa công khai, qua đó bảo đảm an toàn cho dữ liệu được mã hóa.
Chống tấn công bằng tính toán lượng tử:Một số dự án blockchain bắt đầu áp dụng các thuật toán mật mã chống tấn công bằng tính toán lượng tử, chẳng hạn các biến thể an toàn trước lượng tử của thuật toán chữ ký số đường cong elliptic(ECDSA), để ứng phó với mối đe dọa từ tính toán lượng tử trong tương lai.
Khóa công khai dễ phân phối
Tính công khai của khóa công khai: Khóa công khai có thể được công bố cho bất kỳ ai, nên việc phân phối rất thuận tiện. Bất kỳ ai cũng có thể dùng khóa công khai để mã hóa dữ liệu, nhưng chỉ người giữ khóa riêng mới có thể giải mã.
Xác thực danh tính và chữ ký số
Xác thực danh tính:Khóa công khai có thể được dùng làm định danh của người dùng, cho phép người khác xác minh danh tính của họ mà không cần biết khóa riêng. Điều này giúp xây dựng danh tính đáng tin cậy trong các mạng phi tập trung như blockchain.
Chữ ký số:Người nắm giữ khóa riêng có thể dùng khóa riêng để ký thông điệp, còn người khác có thể dùng khóa công khai để xác minh tính xác thực của chữ ký. Điều này bảo đảm tính toàn vẹn của thông điệp và độ tin cậy của nguồn gửi.
Truyền thông an toàn
Liên lạc mã hóa:Khóa công khai có thể dùng để mã hóa thông điệp, chỉ người giữ khóa bí mật tương ứng mới có thể giải mã. Điều này bảo đảm an toàn dữ liệu trong quá trình liên lạc, ngăn những người không được cấp quyền truy cập thông tin.
Kiểm soát tài sản số
Địa chỉ blockchain và khóa riêng:Địa chỉ blockchain thường được suy ra từ khóa công khai, người dùng kiểm soát tài sản số gắn với địa chỉ đó thông qua khóa riêng. Tính bảo mật của khóa riêng có vai trò then chốt đối với sự an toàn của tài sản số.
Tốc độ mã hóa và giải mã
Tương đối chậm:Tốc độ mã hóa và giải mã của mã hóa bất đối xứng chậm hơn nhiều so với mã hóa đối xứng, vì cần thực hiện các phép tính toán học phức tạp hơn. Tuy nhiên, điều này không ảnh hưởng đến việc ứng dụng nó trong các lĩnh vực như truyền thông an toàn và xác thực danh tính.
2.2 Công dụng của mã hóa bất đối xứng
Mã hóa bất đối xứng có hai tình huống sử dụng kinh điển.
Mã hóa:Đây là cách dùng quen thuộc nhất với chúng ta, tức dùng khóa công khai để mã hóa, khóa riêng để giải mã.
Ký số:Do mạng không đáng tin cậy, dữ liệu có thể bị sửa đổi trong quá trình truyền tải, lúc này có thể dùng cặp khóa công khai và khóa riêng theo chiều ngược lại, dùng khóa riêng để ký dữ liệu, dùng khóa công khai để xác minh chữ ký, bảo đảm dữ liệu an toàn và toàn vẹn.
Với công dụng thứ hai, một số bạn sẽ diễn đạt là dùng khóa riêng để mã hóa, dùng khóa công khai để giải mã; dù sao mọi người cũng biết đang nói về cùng một việc.
Thuật toán mã hóa bất đối xứng, dù về lý thuyết có thể dùng để mã hóa dữ liệu và ký số, nhưng trong thực tế, độ phức tạp tính toán cao và hiệu suất thấp trở thành trở ngại chính.
So với thuật toán mã hóa đối xứng, tốc độ tính toán của mã hóa bất đối xứng chậm hơn vài bậc độ lớn, ảnh hưởng nghiêm trọng đến khả năng xử lý khối lượng dữ liệu lớn. Ngoài ra, do quá trình mã hóa và giải mã của thuật toán mã hóa bất đối xứng gắn chặt với độ dài khóa, đồng thời không hỗ trợ chế độ mã hóa theo khối, nó chỉ có thể xử lý lượng dữ liệu nhỏ không vượt quá độ dài khóa, không thể mã hóa lượng lớn dữ liệu.
Để khắc phục hạn chế về hiệu năng của mã hóa bất đối xứng, các hệ thống mã hóa hiện đại thường áp dụng chiến lược mã hóa lai, kết hợp ưu điểm của mã hóa đối xứng và mã hóa bất đối xứng. Trong chiến lược này, mã hóa bất đối xứng chủ yếu được dùng để truyền an toàn một khóa mã hóa đối xứng(tức “thỏa thuận khóa”)cho bên kia. Khi hai bên đã chia sẻ an toàn khóa đối xứng này, họ có thể dùng thuật toán mã hóa đối xứng hiệu quả để mã hóa và giải mã lượng lớn dữ liệu. Cách sử dụng kết hợp này vừa nâng cao hiệu quả mã hóa, vừa tăng tính bảo mật của giao tiếp, được ứng dụng rộng rãi trong nhiều giao thức truyền thông an toàn như SSL/TLS.
Trong lĩnh vực chữ ký số, một số điều chỉnh cũng đã được thực hiện để nâng cao hiệu suất của mã hóa bất đối xứng. Cách làm cụ thể là trước tiên xử lý băm dữ liệu gốc(có thể dùng MD5, SHA, v.v.), thu được một giá trị băm có độ dài cố định. Sau đó, sử dụng thuật toán mã hóa bất đối xứng để mã hóa giá trị băm này, tạo ra chữ ký số. Vì thuật toán băm có thể nén hiệu quả dữ liệu có độ dài bất kỳ thành giá trị băm có độ dài cố định, nên dù dữ liệu gốc lớn đến đâu, độ dài dữ liệu băm vẫn như nhau và quá trình ký vẫn có thể duy trì hiệu suất cao. Khi xác minh chữ ký, chỉ cần tính lại giá trị băm của dữ liệu gốc rồi so sánh với chữ ký sau khi giải mã là có thể nhanh chóng xác định dữ liệu có bị sửa đổi trái phép hay không.
2.3 Ví dụ mã hóa
Ví dụ mã Java sử dụng RSA để mã hóa và giải mã:
public class RsaDemo { public static void main(String[] args) { try { KeyPair keyPair = generateRSAKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); String originalData = "Hello, 江南一点雨!"; String encryptedData = encrypt(publicKey, originalData); String decryptedData = decrypt(privateKey, encryptedData);
System.out.println("加密后的数据: " + encryptedData);
System.out.println("解密后的数据: " + decryptedData);
} catch (Exception e) {
e.printStackTrace();
}
} public static String decrypt(PrivateKey privateKey, String encryptedData) throws Exception { Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData)); return new String(decryptedBytes);
} public static String encrypt(PublicKey publicKey, String data) throws Exception { Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] bytes = cipher.doFinal(data.getBytes()); return Base64.getEncoder().encodeToString(bytes);
} public static KeyPair generateRSAKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048); // 可以指定密钥长度,如2048位
return keyGen.generateKeyPair();
}
}2.4 Phối hợp frontend và backend
Mã hóa ở frontend, giải mã ở backend
Mã phía máy chủ giống với ví dụ trên, điểm khác là sau khi lấy được khóa công khai, chúng ta có thể in khóa đó ra; khóa công khai này sau đó cần được truyền cho phía giao diện:
public static void main(String[] args) { try { KeyPair keyPair = generateRSAKeyPair(); PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate();
System.out.println("公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded())); String originalData = "Hello, 江南一点雨!"; String encryptedData = encrypt(publicKey, originalData); String decryptedData = decrypt(privateKey, encryptedData);
System.out.println("加密后的数据: " + encryptedData);
System.out.println("解密后的数据: " + decryptedData);
} catch (Exception e) {
e.printStackTrace();
}
}Mã phía giao diện như sau:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RSA Encryption Example</title> <!-- 引入jsencrypt库 --> <script src="https://cdn.jsdelivr.net/npm/[email protected]/bin/jsencrypt.min.js"></script></head><body><script> // 重写前端加密方法 function encryptData(publicKey, data) { // 创建一个新的JSEncrypt对象 var encryptor = new JSEncrypt(); // 设置公钥 encryptor.setPublicKey(publicKey); // 加密数据 var encrypted = encryptor.encrypt(data); return encrypted; } // 示例公钥(实际使用时应该替换为服务器提供的公钥) var publicKey = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnP5FOXIghidLDE2FgFwzi4Y+qTdYjnc0eMoiL5i4gdOeTE+7Uu9YvDB51GcBWKD9lvmZr5rX8z0OBpwe526qsTekNpXoIVk9DB34US0HOrAXEEwpUTVuSR656cJGAwmWBkVaalQynEz4Dlzrm53zExVYueruYUuzFyuZDaQcFnl3rWUH/XDkCIe23z0R1TfT3Q2OYNKft0u56r0S/ko99utXuYJK9yowe7QGT6q4cSJwsITQTomCARAq9q+bSNuGEYa4FlYCKIKWIhKbMhz0FYIMB2fJN10GyZbbvKASqeMkuCoD2Efgd8/6uMwOaMcx9LgEkcFaQ3qgDutsPXNUswIDAQAB`; // 要加密的数据 var data = 'Hello, javaboy!'; // 调用加密方法 var encryptedData = encryptData(publicKey, data); // 输出加密后的数据 console.log('Encrypted Data:', encryptedData);</script></body></html>
Mã hóa ở phía máy chủ, giải mã ở phía máy khách
Sử dụng thuật toán RSA để mã hóa dữ liệu trong Java rồi giải mã dữ liệu đó trong JavaScript có nghĩa là phía máy chủ mã hóa bằng khóa công khai của phía giao diện, còn phía giao diện giải mã bằng khóa riêng của mình. Trong tình huống này, khóa riêng ở phía giao diện rất dễ bị đánh cắp, vì vậy không khuyến nghị cách dùng này. Tôi cũng sẽ không đưa ví dụ.
Được rồi, vậy là tôi lại trò chuyện với các bạn một lượt về mã hóa đối xứng và mã hóa bất đối xứng. Anh Tùng đã kiểm thử thành công toàn bộ mã ví dụ ở trên, các bạn có thể dùng để tham khảo.