Triển khai mã hóa RSA bảo vệ thông tin đăng nhập trong ứng dụng Java Web

Trong kiến trúc kết nối giữa thiết bị di động (Android/iOS) và máy chủ Java, việc truyền tải thông tin xác thực người dùng luôn tiềm ẩn rủi ro bảo mật nghiêm trọng. Khi mật khẩu được gửi đi dưới dạng văn bản rõ hoặc chỉ được xử lý bằng các phương pháp đơn giản, chúng rất dễ bị đánh chặn và khai thác trên đường truyền mạng.

Các giải pháp truyền thống như Base64 chỉ thực hiện chuyển đổi ký tự và có thể được giải mã ngay lập tức. Trong khi đó, hàm băm một chiều như MD5 tuy không thể đảo ngược nhưng lại tạo ra lỗ hổng tấn công replay (phát lại). Kẻ tấn công chỉ cần bắt giữ chuỗi băm MD5 và gửi lại cho máy chủ là có thể vượt qua kiểm tra xác thực mà không cần biết mật khẩu gốc của người dùng.

Để triệt tiêu các lỗ hổng này, mã hóa bất đối xứng RSA là lựa chọn tối ưu. Khác với mã hóa đối xứng dùng chung một khóa, RSA sử dụng cặp khóa gồm public key (công khai) và private key (bí mật). Thông tin được mã hóa bằng public key chỉ có thể được giải mã bằng private key tương ứng. Vì private key không bao giờ được truyền qua mạng, nên ngay cả khi public key bị lộ, dữ liệu nhạy cảm vẫn được bảo vệ an toàn. Với độ dài khóa 1024-bit, RSA được xem là đủ mạnh cho việc bảo vệ thông tin xác thực trong các hệ thống doanh nghiệp.

Quy trình tích hợp RSA vào luồng đăng nhập thường diễn ra theo các bước sau:

  • Thiết bị đầu cuối gửi yêu cầu khởi tạo khóa đến máy chủ.
  • Máy chủ tạo cặp khóa RSA, giữ lại private key và chỉ trả về các tham số xây dựng public key (mô số và số mũ) cho client.
  • Client sử dụng tham số nhận được để tái tạo public key, mã hóa mật khẩu người dùng, rồi gửi ciphertext về máy chủ.
  • Máy chủ dùng private key lưu trữ để giải mã ciphertext, lấy mật khẩu gốc và tiến hành xác thực.

Dưới đây là bộ công cụ Java xử lý toàn bộ quy trình trên. Mã nguồn đã được tái cấu trúc để đảm bảo tính ổn định, sử dụng chuẩn đệm PKCS1Padding và định dạng Hex để tránh lỗi tương thích giữa các nền tảng JDK và Android.

import java.security.*;
import java.security.spec.*;
import javax.crypto.Cipher;
import java.util.*;
import java.nio.charset.StandardCharsets;

public class RsaSecurityUtils {

    private static final String ALGORITHM = "RSA";
    private static final int KEY_SIZE = 1024;

    /**
     * Khởi tạo cặp khóa RSA 1024-bit
     */
    public static Map<String, Key> generateKeyPair() throws GeneralSecurityException {
        Map<String, Key> store = new HashMap<>();
        KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGORITHM);
        kpg.initialize(KEY_SIZE);
        KeyPair kp = kpg.generateKeyPair();
        store.put("public", kp.getPublic());
        store.put("private", kp.getPrivate());
        return store;
    }

    /**
     * Xây dựng Public Key từ mô số và số mũ nhận được từ server
     */
    public static PublicKey reconstructPublicKey(String modulusStr, String exponentStr) throws GeneralSecurityException {
        BigInteger moSo = new BigInteger(modulusStr);
        BigInteger soMu = new BigInteger(exponentStr);
        RSAPublicKeySpec spec = new RSAPublicKeySpec(moSo, soMu);
        KeyFactory kf = KeyFactory.getInstance(ALGORITHM);
        return kf.generatePublic(spec);
    }

    /**
     * Xây dựng Private Key từ mô số và số mũ bí mật
     */
    public static PrivateKey reconstructPrivateKey(String modulusStr, String exponentStr) throws GeneralSecurityException {
        BigInteger moSo = new BigInteger(modulusStr);
        BigInteger soMu = new BigInteger(exponentStr);
        RSAPrivateKeySpec spec = new RSAPrivateKeySpec(moSo, soMu);
        KeyFactory kf = KeyFactory.getInstance(ALGORITHM);
        return kf.generatePrivate(spec);
    }

    /**
     * Mã hóa văn bản bằng Public Key
     * Tự động chia nhỏ dữ liệu nếu vượt quá giới hạn chiều dài khóa
     */
    public static String encryptData(String plainText, PublicKey pubKey) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, pubKey);

        int doDaiKhoa = ((RSAPublicKey) pubKey).getModulus().bitLength() / 8;
        int gioHanChuoi = doDaiKhoa - 11; // Giới hạn padding PKCS1
        List<String> cacPhan = chiaChuoi(plainText, gioHanChuoi);

        StringBuilder chuoiMaHoa = new StringBuilder();
        for (String phan : cacPhan) {
            byte[] bytesPhan = phan.getBytes(StandardCharsets.UTF_8);
            byte[] bytesMaHoa = cipher.doFinal(bytesPhan);
            chuoiMaHoa.append(maHex(bytesMaHoa));
        }
        return chuoiMaHoa.toString();
    }

    /**
     * Giải mã chuỗi Hex bằng Private Key
     */
    public static String decryptData(String hexCipher, PrivateKey privKey) throws GeneralSecurityException {
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privKey);

        int doDaiKhoa = ((RSAPrivateKey) privKey).getModulus().bitLength() / 8;
        byte[] rawData = giaiMaHex(hexCipher);
        List<byte[]> cacPhanByte = chiaMangByte(rawData, doDaiKhoa);

        StringBuilder chuoiGoc = new StringBuilder();
        for (byte[] phan : cacPhanByte) {
            byte[] bytesGoc = cipher.doFinal(phan);
            chuoiGoc.append(new String(bytesGoc, StandardCharsets.UTF_8));
        }
        return chuoiGoc.toString();
    }

    // Các phương thức hỗ trợ xử lý chuỗi và byte array
    private static List<String> chiaChuoi(String input, int size) {
        List<String> result = new ArrayList<>();
        for (int i = 0; i < input.length(); i += size) {
            result.add(input.substring(i, Math.min(i + size, input.length())));
        }
        return result;
    }

    private static List<byte[]> chiaMangByte(byte[] input, int size) {
        List<byte[]> result = new ArrayList<>();
        for (int i = 0; i < input.length; i += size) {
            int end = Math.min(i + size, input.length);
            result.add(Arrays.copyOfRange(input, i, end));
        }
        return result;
    }

    private static String maHex(byte[] bytes) {
        StringBuilder hex = new StringBuilder();
        for (byte b : bytes) {
            hex.append(String.format("%02x", b));
        }
        return hex.toString();
    }

    private static byte[] giaiMaHex(String hex) {
        byte[] bytes = new byte[hex.length() / 2];
        for (int i = 0; i < hex.length(); i += 2) {
            bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return bytes;
    }
}

Tại tầng xử lý yêu cầu đăng nhập trên máy chủ, logic khởi tạo và phân phối khóa sẽ được triển khai như sau:

Map<String, Key> khoKhóa = RsaSecurityUtils.generateKeyPair();
PublicKey cuaServer = khoKhóa.get("public");
PrivateKey thuKey = khoKhóa.get("private");

// Lấy thông số để truyền xuống client
String moSoStr = ((RSAPublicKey) cuaServer).getModulus().toString();
String soMuStr = ((RSAPublicKey) cuaServer).getPublicExponent().toString();
String soMuThuStr = ((RSAPrivateKey) thuKey).getPrivateExponent().toString();

// Lưu trữ số mũ bí mật và mô số để tái tạo khóa khi cần giải mã
// Client sẽ nhận moSoStr và soMuStr, gọi reconstructPublicKey() để mã hóa mật khẩu

Khi client gửi yêu cầu đăng nhập, hệ thống sẽ nhận chuỗi mật khẩu đã được mã hóa hex. Máy chủ chỉ việc tái tạo private key từ cặp tham số (mô số và số mũ bí mật) đã lưu trữ trước đó, sau đó gọi phương thức giải mã để khôi phục mật khẩu gốc. Giá trị trả về sẽ được so sánh với dữ liệu trong cơ sở dữ liệu để đưa ra kết quả xác thực. Quá trình này đảm bảo mật khẩu không bao giờ xuất hiện dưới dạng văn bản rõ trên kênh truyền tải, đồng thời vô hiệu hóa hoàn toàn các kịch bản bắt gói tin hay phát lại dữ liệu.

Thẻ: Java RSA Bảo mật ứng dụng Mã hóa bất đối xứng Tương tác Client-Server

Đăng vào ngày 19 tháng 5 lúc 08:03