Header Ads

Seo Services

Chào anh em!

Bài viết hôm nay mình sẽ cùng anh em tìm hiểu về một chủ đề nữa liên quan đến bảo mật đó là làm sao để băm (hashing) một password. 

Mà cụ thể ở đây chúng ta sử dụng ngôn ngữ lập trình Java và cách để triển khai các thuật toán hashing password đối với ngôn ngữ này.

Okay, bắt đầu luôn thôi nhỉ!

1. Hashing là gì?

Mình còn nhớ, hồi đi học chưa biết đến mã hóa hay hashing là gì nên toàn lưu password vào database theo kiểu có gì lưu đấy, tức là lưu dạng raw đó anh em 😁

Và rồi cái gì đến cũng phải đến, khi học môn an toàn bảo mật thông tin thầy giáo test ứng dụng của mấy anh em team mình thì cả đám ăn ngay điểm 4 vì mắc cái lỗi cơ bản nhất quả đất trong khi ứng dụng thì nhiều tính năng hay ho lắm.

Đó chia sẻ câu chuyện ngắn để anh em nắm được việc mã hóa hay hash các thông tin nhạy cảm như password trước khi lưu trữ là một yêu cầu cơ bản trong an toàn và bảo mật thông tin.

Okay, vậy hashing là gì?

Dịch ra thì "hash" có nghĩa "băm" mà anh em hình dung khi băm một cái gì đó ra thì rất khó để khôi phục lại hình dạng ban đầu đúng không.

Từ đặc điểm đó anh em có thể hiểu trong an toàn và bảo mật thông tin thì hashing là quá trình sinh ra một chuỗi (string) mới từ một thông tin (message) cho trước bằng việc sử dụng một thuật toán băm (cryptographic hash algorithm).

Đặc điểm nổi bật của hàm băm (hash function):

i. Với cùng một thông tin và sử dụng cùng một hàm băm sẽ luôn luôn cho ra cùng một kết quả băm giống nhau.

ii. Hàm băm là một chiều: Tức khi đã băm ra anh em sẽ không thể sử dụng kết quả đó để truy ngược lại thông tin được băm ban đầu.

iii. Tính nhiễu cao: Chỉ với một thay đổi nhỏ trong message ban đầu cũng có thể dẫn tới hai kết quả rất khác nhau của hàm băm.

iv. Tính xung đột: Và tất nhiên hai message khác nhau sẽ không bao giờ cho ra một kết quả băm giống nhau. 

2. MD5 

MD5 là một thuật toán băm (hashing algorithm) với đầu ra là một chuỗi băm có kích thước 128-bit (16 bytes)

Ưu điểm của MD5 là nhanh tuy nhiên gần đây người ta nhận thấy MD5 không còn đảm bảo được tính xung đột hay cụ thể hơn là hai input khác nhau có thể cho ra cùng một output giống nhau.

Ngoài ra do MD5 "nhanh" nên có thể bị tấn công bằng phương pháp vét cạn (brute-force) dễ dàng. Vì vậy hiện nay người ta khuyên nên hạn chế sử dụng thuật toán này để hash thông tin nhạy cảm.

Trong Java, chúng ta có thể sử dụng MessageDigest để triển khai thuật toán MD5 như sau:

public class MD5Utils {
	private static final Charset UTF_8 = StandardCharsets.UTF_8;

    private static byte[] digest(byte[] input) {
        MessageDigest md;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException(e);
        }
        byte[] result = md.digest(input);
        return result;
    }

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

    public static void main(String[] args) {
        String plainText = "B2A@123";
        System.out.println("Input (string): " + plainText);
        System.out.println("Input (length): " + plainText.length());

        byte[] md5InBytes = MD5Utils.digest(plainText.getBytes(UTF_8));
        
        System.out.println("MD5 (hex): " + bytesToHex(md5InBytes));
        System.out.println("MD5 (length): " + md5InBytes.length); //16 bytes, 128 bits
    }
}

Output:

Input (string): B2A@123
Input (length): 7
MD5 (hex): e3fd1fde63c08e43058c7c9621410053
MD5 (length): 16

3. SHA-512

Chúng ta biết rằng ngày nay các máy tính hiện đại có sức mạnh tính toán khủng khiếp như thế nào! Vì vậy chỉ với phương pháp vét cạn thì thời gian để giải mã MD5 đã nhanh hơn rất nhiều.

Đó là chưa kể còn rất nhiều lỗ hổng trong các thuật toán cũ vậy nên các nhà nghiên cứu đã phát triển các thuật toán khác "an toàn" hơn. Và ở đây chúng ta có họ nhà SHA (Secure Hash Algorithm)

Bài viết này mình sẽ cùng anh em tìm hiểu về SHA-512 thuật toán được đánh giá là mạnh nhất trong cả họ khi triển khai với Java. Anh em có thể tham khảo thêm các thuật toán bảo mật tiêu chuẩn trong Java tại đây.

3.1 - Salt

Trước khi triển khai thuật toán SHA-512 trong Java anh em lưu ý thêm cho mình về một thuật ngữ gọi là "salt" trong an toàn bảo mật thông tin.

Có thể hiểu vui vui như này, nếu muốn món thịt băm ăn ngon hơn thì phải thêm gia vị, gia vị chúng ta dùng ở đây chính là các hạt tiêu (salt) hay anh em hiểu là muối cũng được.

Ở góc độ kỹ thuật, tiêu (salt) là các chuỗi ngẫu nhiên được sinh ra và chúng ta sẽ thêm nó vào chuỗi được băm để tăng tính nhiễu của output.

Điều này giúp cho việc tấn công vét cạn (brute-force) trở nên bất khả thi trong thời gian cho phép và tấn công rainbow cũng rất rất khó khăn.

Để sinh ra chuỗi salt trong Java chúng ta sử dụng class SecureRandom thuộc package java.security như sau:

public static byte[] generateRandomSalt(int numBytes) {
	byte[] salt = new byte[numBytes];
	new SecureRandom().nextBytes(salt);
	return salt;
}

3.2 - Triển khai SHA-512 trong Java.

Tương tự như MD5 chúng ta cũng có thể sử dụng class MessageDegist để triển khai thuật toán SHA-512 trong Java như sau:

public class SHAUtils {
	private static final Charset UTF_8 = StandardCharsets.UTF_8;

	public static byte[] generateRandomSalt(int numBytes) {
		byte[] salt = new byte[numBytes];
		new SecureRandom().nextBytes(salt);
		return salt;
	}

	private static byte[] digest(byte[] input, String algorithm) {
		MessageDigest md;
		try {
			md = MessageDigest.getInstance(algorithm);
			byte[] salt = generateRandomSalt(16);
			md.update(salt);
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalArgumentException(e);
		}
		byte[] result = md.digest(input);
		return result;
	}

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

	public static void main(String[] args) {
		String plainText = "B2A@123";
		System.out.println("Input (string): " + plainText);
		System.out.println("Input (length): " + plainText.length());

		byte[] sha512InBytes = SHAUtils.digest(plainText.getBytes(UTF_8), "SHA-256");

		System.out.println("SHA-512 (hex): " + bytesToHex(sha512InBytes));
		System.out.println("SHA-512 (length): " + sha512InBytes.length);
	}
}

Output:

Input (string): B2A@123
Input (length): 7
SHA-512 (hex): b92aa165fb27b90069248a282aa2604dfef666fcb68070c763ddf81ea1294e442546f6549a260ea8e704570c57a021a80accb5317d86a689cc0de74819412c62
SHA-512 (length): 64

Lưu ý: Mặc dù việc sử dụng các thuật toán họ SHA hay cụ thể là SHA-512 là một lựa chọn phù hợp nếu kết hợp với sử dụng salt. Nhưng nếu sức mạnh của máy tính tăng lên thì có lẽ lại không còn là lựa chọn phù hợp nữa. Chính vì vậy chúng ta có các thuật toán băm "linh hoạt" hơn, chậm (chậm để giải mã) hơn ngay bên dưới đây.

4. PBKDF2, BCrypt, and SCrypt

4.1 - Ưu điểm của PBKDF2, BCrypt và SCrypt là gì?

Như mình đã trình bày bên trên thì MD5 hay SHA-512 sẽ bị "yếu thế" nếu như khả năng xử lý của máy tính ngày càng mạnh hơn.

Vậy câu hỏi đặt ra là các thuật toán băm phải làm sao để có thể linh hoạt thay đổi input và làm tăng thời gian xử lý của máy tính?

PBKDF2, BCrypt hay SCrypt là những thuật toán băm làm được điều này! 

4.1 - Triển khai PBKDF2 trong Java

Hiện nay việc mã hóa password sử dụng các thuật toán băm gần như không thể thiếu salt, đối với PBKDF2 cũng vậy.

Chúng ta sẽ triển khai PBKDF2 trong Java như sau:

public static String generateHash(String pTextPassword) {
	byte[] salt = generateRandomSalt(16);
	
	KeySpec spec = new PBEKeySpec(pTextPassword.toCharArray(), salt, 10000, 256);
	SecretKeyFactory factory;
	byte[] hash = null;
	try {
		factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
		hash = factory.generateSecret(spec).getEncoded();
	} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
		e.printStackTrace();
	}
	
	return bytesToHex(hash);
}

Các hàm generateRandomSalt()bytesToHex() mình đã trình bày ở các phần trước.

Output:

pText: B2A@123 => hash value: 36863a0343aab9ebe180be593dd2bfeafcdab63d04c14a53a016f07cdd6a8bdc
pText: B2A@123. => hash value: 3c2df887c9e8cb057ae1390ee37e4bafe177196ba7b82d5a0336787a995229db
pText: B2A@123! => hash value: 49a820c8229a91651eef908d5d94382a7a0ff8bfdbfad47a0ca6f8f0b624bf50
pText: B2A@123  => hash value: 6b97bede2f1c423bed8b711684ada39c8d99d98bdabcb447f4a18daf282f20b3

Anh em có thể thấy input của mình chỉ khác nhau một chút nhưng output thì khác nhau rất nhiều. Đây chính là điểm mạnh của thuật toán này.

Lưu ý: Ở đây hàm mã hóa có 4 tham số.

- Đầu tiên là input (raw password)

- Thứ hai là salt được sinh random từ hàm generateRandomSalt()

- Thứ ba là số vòng lặp của thuật toán. Dựa vào tham số này chúng ta có thể tăng hoặc giảm thời gian nhận được kết quả băm

- Cuối cùng là kích thước của giá trị băm (tính theo đơn vị bits)

4.2 - Triển khai BCrypt và Scrypt trong Java!

Rất tiếc là các thuật toán BCrypt và Scrypt chưa được hỗ trợ trực tiếp trong Java. Mình sẽ cùng anh em tìm hiểu cách triển khai các thuật toán này thông qua Spring Security ở phần tiếp theo.

5. Sử dụng Spring Security

Spring Security hỗ trợ cả ba thuật toán PBKDF2, BCrypt, SCrypt  thông qua interface PasswordEncoder và cụ thể sẽ có 3 class triển khai interface này như sau:

Pbkdf2PasswordEncoder : thuật toán PBKDF2

BCryptPasswordEncoder : thuật toán BCrypt

SCryptPasswordEncoder : thuật toán SCrypt

Ở đây, đối với thuật toán PBKDF2 mình đã triển khai ở phần 4 rồi nên ở phần 5 này mình sẽ lấy ví dụ với thuật toán BCrypt. Còn với SCrypt cũng sẽ tương tự như với BCrypt anh em nhé.

public class BcryptUtil {
    public static String generateHash(String rawPassword, BCryptPasswordEncoder.BCryptVersion version,
                                            int strength, SecureRandom salt) {
        BCryptPasswordEncoder bCrypt = new BCryptPasswordEncoder(version, strength, salt);
        String hashPassword = bCrypt.encode(rawPassword);

        return hashPassword;
    }

    public static void main(String[] args) {
        String rawPassword = "B2A@123";
        byte[] bytes = new byte[16];
        SecureRandom random = new SecureRandom(bytes);
        int length = 10; // between 4 and 31, default: 10

        String hashPassword = generateHash(rawPassword, BCryptPasswordEncoder.BCryptVersion.$2A, length, random);

        System.out.println(hashPassword);
    }
}

Output:

$2a$10$5IqOyzE9r6J9KQiRjXde9u47uiI9GhqemsDTlGKASrMZxjYK4yAqq

Lưu ý:

i. Để sử dụng được các class này anh em nhớ là phải thêm thư viện của spring security crypto tại đây nhé.

ii. Ở đây mình truyền vào 3 tham số: version, strength, salt

Trong đó, version là phiên bản của thuật toán với BCrypt chúng ta có 2a, 2b, 2y. Tiếp theo strenght là độ mạnh của thuật toán hay nói cách khác là thời gian xử lý trả ra kết quả, strength được giới hạn từ 4 đến 31 đối với BCrypt và mặc định có giá trị là 10. Cuối cùng không thể thiếu salt, BCrypt và SCrypt đều có cơ chế tự sinh ra salt (chúng ta chỉ cần truyền kích thước) và salt này sẽ được gắn lại vào đầu của chuỗi hash kết quả. 

6. Tham khảo

Bên dưới là một vài tài liệu mình tham khảo cho bài viết này. Hẹn gặp lại anh em trong các bài viết tiếp theo nhé.

https://www.baeldung.com/java-password-hashing

https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html

Thanks all! ❤️❤️❤️

Không có nhận xét nào:

Được tạo bởi Blogger.