- Published on
How to create a secure code and protect data security.
- Authors
- Name
- David Nguyen
Thời gian vừa rồi trong dự án ở công ty mình có đảm nhiệm phần tích hợp với hệ thống của đối tác. Hiểu nôm na việc tích hợp này là hệ thống bên mình sẽ gọi các APIs của hệ thống bên đối tác để gửi và nhận dữ liệu đồng thời thực hiện một số nghiệp vụ nhất định.
Chính vì vậy yêu cầu đặt ra là phải đảm bảo tính toàn vẹn của gói tin và bên hệ thống đối tác có yêu cầu bên mình phải sinh ra một đoạn secure code theo quy tắc mà họ định nghĩa sau đó gửi kèm nó theo gói tin khi gọi APIs bên họ.
Bài viết hôm nay mình sẽ cùng anh em tìm hiểu lý do cũng như cách để chúng ta tạo ra được đoạn secure code như vậy.
Note: À mình lưu ý là có rất nhiều cách sinh ra secure code nha, đây chỉ là một trong những cách mà mình sẽ giới thiệu với anh em thôi.
Okay, bắt đầu ngay thôi nhỉ!
Table of Contents
1. Secure Code là gì và để làm gì?
1.1 - Là gì?
Trước tiên thì "secure code" chỉ là cái tên thôi anh em nhé, anh em có thể gọi nó bằng nhiều cái tên khác (secret code, private code) miễn sao anh em hiểu vai trò của nó là được.
Chi tiết hơn thì trong bài viết này secure code là một chuỗi ký tự và là kết quả sau khi chúng ta băm (hash) một đối tượng thông tin khác (chuỗi, files) bằng thuật toán băm (SHA256, MD5, SHA512...)
Tại sao lại là băm (hashing) mà không phải là mã hóa? Câu hỏi này tý mình sẽ trả lời ở mục 1.2 nhé.
Còn gì nữa nhỉ 🤔 À đấy là kết quả của thuật toán băm thì chắc anh em cũng biết rồi nhỉ! Nó có thể trả về một chuỗi text hoặc một chuỗi hex và chúng ta có thể cố định độ dài của chuỗi này.
Okay, mình nghĩ vậy là đủ để anh em hình dung được secure code là gì rồi ha. Sâu xa hơn thì anh em có thể google thêm cho tường tận chân lông kẽ tóc nhé 😉
1.2 - Để làm gì?
Như mình đã đề cập từ đầu bài viết thì secure code chúng ta đang nói đến được sử dụng để đảm bảo tính toàn vẹn của gói tin.
Thế nào là tính toàn vẹn và tại sao chúng ta phải đảm bảo thì mình sẽ giải thích ngay sau đây:
Thứ nhất là tính toàn vẹn, mình lấy ví dụ thực tế cho dễ hiểu nhé. Chẳng hạn mình gọi API đăng nhập của hệ thống đối tác và API đó yêu cầu mình phải gửi các thông tin như username, password.
Vấn đề là hai hệ thống khi đó sẽ giao tiếp với nhau qua môi trường internet và mình giả sử tin tặc hay bất cứ một lý do đường truyền nào đó khiến cho nội dung gói tin (username, password hoặc các thông tin kèm theo) bị thay đổi thì sẽ như thế nào!? Đó chính là tính toàn vẹn.
Thứ hai, tại sao chúng ta phải đảm bảo tính toán vẹn? Theo mình có hai lý do chính đó là đứng ở góc độ bảo mật và ở góc độ validate dữ liệu.
Cụ thể thì anh em biết rằng đối với các thuật toán băm (hashing) chúng ta sẽ không thể giải mã ngược lại để có được thông tin đầu vào từ kết quả băm. Cách duy nhất để so khớp thông tin đó là ở cả hai phía phải có thông tin đầu vào chính xác sau đó cùng băm (thông qua một thuật toán, quy tắc cụ thể) và so sánh kết quả hàm băm xem có trùng khớp hay không.
Lợi dụng đặc tính này của các thuật toán băm chúng ta sẽ bảo đảm được thông tin gửi đi là bảo mật và từ đó việc validate dữ liệu chỉ được diễn ra ở hai đầu hai hệ thống.
2. Ví dụ
2.1 - Bài toán
Trong bài viết hôm nay mình sẽ lấy ví dụ mà mình từng làm khi tích hợp với hệ thống của đối tác bên mình. Bài toán họ đưa ra như sau:
Hệ thống bên họ cấp cho bên mình một số APIs để thực hiện các chức năng thanh toán dịch vụ của người dùng (vd: thanh toán tiền điện, tiền nước...)
Giả sử trường hợp thanh toán điện yêu cầu truyền những thông tin (ví dụ thôi anh em nhé) dưới dạng một JSON như sau:
{
"version":"2.0.0",
"partnerId":"202131",
"serviceId":"200",
"channelId":"10",
"paymentCode":"10354395",
"transDateTime":"20220620101832",
"secureCode":"f78fc10408cf6c01627d2323f43de5bd1e91c69d6fda079b4a99f5e6b2cc13f7"
}
Anh em không cần quan tâm những trường này có ý nghĩa gì mà chỉ cần tập trung vào trường secureCode
Trường này sẽ được sinh ra với quy tắc sau:
Mình phải kết hợp tất cả thông tin của các trường lại với nhau theo đúng thứ tự các trường và phân cách bởi dấu | (hoặc bất cứ ký tự nào tùy thuộc vào quy tắc), cuối cùng là một chuỗi secret key được cấp bí mật giữa hai hệ thống để tạo thành một chuỗi duy nhất và thực hiện băm chuỗi đó với thuật toán SHA256 để có được secure code
Ví dụ với dữ liệu như bên trên thì chuỗi trước khi băm của mình có dạng:
2.0.0|202131|200|10|10354395|20220620101832
Và nếu mình có secret key là 8dka089f2960d1s148c06b343da1201 thì chuỗi hoàn chỉnh trước khi băm sẽ như sau:
2.0.0|202131|200|10|10354395|20220620101832|8dka089f2960d1s148c06b343da1201
Giờ đây anh em đem chuỗi này đi băm với thuật toán SHA256 sẽ có được secure code như ví dụ bên trên của mình. Câu hỏi là băm ở đâu 😁 Anh em có thể tham khảo trang này để test thử (lưu ý là chọn input dạng text nha)
Còn tất nhiên trong bài viết hôm nay mình sẽ cùng anh em triển khai code để giải bài toán này.
2.2 - Coding
Hầu hết các ngôn ngữ lập trình đều hỗ trợ các thuật toán băm và trong bài viết này mình vẫn sẽ sử dụng Java là ngôn ngữ để triển khai code nha anh em.
Đầu tiên thì anh em nào code Java đều biết chúng ta phải tạo ra một đối tượng (object) để có thể "bọc" dữ liệu và gửi đi. Trong ví dụ này giả sử mình tạo đối tượng Request với các thông tin như sau:
@Data
public class Request {
private String version;
private String partnerId;
private String serviceId;
private String channelId;
private String paymentCode;
private String transDateTime;
private String secureCode;
}
Note: Ở đây mình có sử dụng annotation @Data của thư viện lombok để đơn giản hóa việc viết các hàm setter, getter. Anh em nào chưa biết có thể tham khảo thêm nhé.
Okay sau tạo xong đối tượng thì quay lại và phân tích xem bài toán của chúng ta yêu cầu gì nào?
Chúng ta phải tạo ra được một chuỗi từ giá trị của các trường dữ liệu theo quy tắc bên trên (đúng thứ tự và phân cách bởi dấu | ) sau đó ghép thêm secret key vào cuối.
Okay, dễ thôi! Chúng ta chỉ cần override lại hàm toString() của đối tượng đó như sau là xong?
@Data
public class Request {
private String version;
private String partnerId;
private String serviceId;
private String channelId;
private String paymentCode;
private String transDateTime;
private String secureCode;
@Override
public String toString() {
return version + "|" + partnerId + "|"+ serviceId + "|"
+ channelId + "|" + paymentCode + "|"+ transDateTime;
}
}
Lưu ý ở đây chúng ta sẽ không sử dụng trường sercureCode trong hàm toString() vì bản chất như mình đã trình bày giá trị trường này sẽ được chúng ta sinh ra thông qua thuật toán băm sau đó mới được set ngược trở lại và gửi theo gói tin.
Hai nữa việc sử dụng hàm toString() rất đơn giản để chúng ta đáp ứng được bài toán nhưng rõ ràng nó hơi "thủ công" vì phụ thuộc vào từng đối tượng cụ thể. Nói cách khác chúng ta phải override hàm này cho từng đối tượng khác nhau. Vậy giả sử có hàm trăm đối tượng thì sẽ như thế nào?
Không lẽ tay to viết hàm toString() cho từng đối tượng!? 🤔 Okay, phần này mình sẽ đưa ra giải pháp cho anh em sau, tạm thời chúng ta tập trung vào bài toán và sử dụng hàm toString() như bên trên nhé.
Để sử dụng thuật toán băm trong Java chúng ta có thể sử dụng class MessageDigest. Và đoạn code hoàn chỉnh để sinh secure code của mình như bên dưới.
public static String generateSecureCode(String strInput, String secretKey, String algorithm) {
String resultStr = strInput + "|" + secretKey;
// sử dụng thuật toán băm để băm chuỗi đầu vào
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
byte[] bytes = md.digest(resultStr.getBytes(StandardCharsets.UTF_8));
// chuyển từ bytes -> text
BigInteger number = new BigInteger(1, bytes);
StringBuilder hexString = new StringBuilder(number.toString(16));
while (hexString.length() < 64)
hexString.insert(0, '0');
return hexString.toString();
}
Bây giờ để đơn giản mình viết hàm main để mô tả đối tượng và cách chúng ta sinh secure code như sau:
public static void main(String[] args) {
Request request = new Request();
request.setVersion("2.0.0");
request.setPartnerId("202131");
request.setServiceId("200");
request.setChannelId("10");
request.setPaymentCode("10354395");
request.setTransDateTime("20220620101832");
String secureCode = generateSecureCode(request.toString(), SECRET_KEY, SHA_256);
System.out.println(secureCode);
request.setSecureCode(secureCode); // set lại secure code cho đối tượng request
}
Sau khi chạy hàm main()
anh em có thể thấy secure code được sinh ra giống với như trong ví dụ của mình. Anh em cũng có thể thử đổi bất kỳ giá trị của trường nào đó hoặc thứ tự các trường thì đều nhận về một kết khác nhau. Chính điều này giúp cho các hệ thống kiểm tra và đảm được tính toàn vẹn của tập dữ liệu khi trao đổi thông tin với nhau.
Bonus: Như mình đã đề cập việc sử dụng hàm toString() tuy đơn giản nhưng lại khá "thủ công" và để bớt "tay to" hơn thì sẽ sử dụng Java Reflection để tạo ra chuỗi trước khi băm theo yêu cầu như sau:
Class<Object> clazz = (Class<Object>) obj.getClass();
Field[] fields = clazz.getDeclaredFields();
List<Object> fieldValues = new ArrayList<>();
try {
for (Field field : fields)
if (!IGNORE_FIELDS.equals(field.getName())) {
Field declaredField = obj.getClass().getDeclaredField(field.getName());
declaredField.setAccessible(true);
// trường hợp trường dữ liệu kiểu String
if(declaredField.get(obj) instanceof String) {
String value = (String) declaredField.get(obj);
fieldValues.add(value);
}
// trường hợp trường dữ liệu kiểu Integer
if(declaredField.get(obj) instanceof Integer) {
Integer value = (Integer) declaredField.get(obj);
fieldValues.add(value);
}
}
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
fieldValues.forEach(field -> resultBuilder.append(field).append("|"));
resultBuilder.append(secretKey);
Nhìn hơi phức tạp nhỉ, nếu anh em nào chưa nắm được về Java Reflection thì có thể google thêm hoặc skip qua đoạn này cũng được nha. Còn anh em nào hiểu rồi thì nhìn đoạn code này cũng biết mình đang làm gì! 😉😁
Ở đây đơn giản mình đọc các trường (fields) từ đối tượng truyền vào (trong ví dụ của chúng ta là đối tượng Request) và lấy ra giá trị của chúng. Mình sẽ phải kiểm tra kiểu dữ liệu của từng field và ép chúng về kiểu tương ứng.
Với mỗi field như thế sẽ "nhét" giá trị của field đó vào một List (ngoại trừ trường secureCode vì trường này không sử dụng và mình có check ở đoạn IGNORE_FIELDS) và cuối cùng mình chỉ cần đọc từ List đó ra rồi tạo chuỗi như mình mong muốn.
Note: Nếu sử dụng Java Reflection thì anh em phải lưu ý định nghĩa các trường trong đối tượng sao cho đúng thứ tự nếu không Java Reflection sẽ đọc sai thứ tự trường và sinh sai secure code đó.
Toàn bộ code anh em có thể tham khảo tham khảo bên dưới nhé.
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Util {
private static final String SECRET_KEY = "8dka089f2960d1s148c06b343da1201";
private static final String SHA_256 = "SHA-256";
private static final List<String> IGNORE_FIELDS = Arrays.asList("secureCode");
public static String generateSecureCode(Object obj, String secretKey, String algorithm) {
StringBuilder resultBuilder = new StringBuilder();
Class<Object> clazz = (Class<Object>) obj.getClass();
Field[] fields = clazz.getDeclaredFields();
List<Object> fieldValues = new ArrayList<>();
try {
for (Field field : fields)
if (!IGNORE_FIELDS.contains(field.getName())) {
Field declaredField = obj.getClass().getDeclaredField(field.getName());
declaredField.setAccessible(true);
// trường hợp trường dữ liệu kiểu String
if (declaredField.get(obj) instanceof String) {
String value = (String) declaredField.get(obj);
fieldValues.add(value);
}
// trường hợp trường dữ liệu kiểu Integer
if (declaredField.get(obj) instanceof Integer) {
Integer value = (Integer) declaredField.get(obj);
fieldValues.add(value);
}
}
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
fieldValues.forEach(field -> resultBuilder.append(field).append("|"));
resultBuilder.append(secretKey);
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
String resultStr = resultBuilder.toString();
byte[] bytes = md.digest(resultStr.getBytes(StandardCharsets.UTF_8));
BigInteger number = new BigInteger(1, bytes);
StringBuilder hexString = new StringBuilder(number.toString(16));
while (hexString.length() < 64)
hexString.insert(0, '0');
return hexString.toString();
}
public static void main(String[] args) {
Request request = new Request();
request.setVersion("2.0.0");
request.setPartnerId("202131");
request.setServiceId("200");
request.setChannelId("10");
request.setPaymentCode("10354395");
request.setTransDateTime("20220620101832");
String secureCode = generateSecureCode(request, SECRET_KEY, SHA_256);
System.out.println(secureCode);
request.setSecureCode(secureCode); // set lại secure code cho đối tượng request
}
}
3. Lời kết
Vậy là trong bài viết này mình đã cùng anh em tìm hiểu lý do cũng như cách để chúng ta sinh ra một đoạn secure code dựa vào thuật toán băm nhằm hỗ trợ đảm bảo tính toàn vẹn cũng như đúng đắn của gói tin khi hai hay nhiều hệ thống giao tiếp với nhau.
Hẹn gặp lại anh em trong các bài viết tiếp theo nhé! Thanks all!