Chủ Nhật, 23 tháng 2, 2020

Thuật toán mã hóa bất đối xứng RSA

Trong bài này, chúng ta đã được tìm hiểu symmetric encryption là gì, asymmetric encryption là gì rồi. Nhưng chúng ta vẫn chưa biết được, làm thế nào để người ta có thể tạo ra bộ key mã hóa và giải mã.
Trong bài hôm nay, mình sẽ giới thiệu cho các bạn 1 trong những thuật toán asymmetric được sử dụng để tạo ra cặp key mã hóa và giải mã. Đó chính là RSA Algorithm.
 Rivest–Shamir–Adleman (RSA) là một thuật toán mã hóa bất đối xứng. Sỡ dĩ nó có tên như vậy là vì nó được ghép từ tên của 3 người tác giả.

RSA hoạt động như thế nào?

 RSA hoạt động dựa vào việc tìm kiếm 5 con số quan trọng: p ,q , N, ed. Để tìm được 5 con số này, bạn cần thực hiện các bước sau:
Bước 1: chọn ra 2 con số nguyên tố tùy ý. 2 con số này chính là p và q. Ví dụ mình chọn p = 2 và q = 7

Bước 2: Tìm N theo công thức: N = p*q . Lúc này, ta có N = 2*7 = 14.

Bước 3: Tính $\phi(N)$ . $\phi(N)$ là hàm phi. Hàm phi của một số nguyên dương N là số các số nguyên dương nhỏ hơn N và là sô nguyên tố cùng nhau với N.Hai số được gọi là số nguyên tố cùng nhau khi mà chúng có ước số chung lớn nhất là 1.
Ví dụ: $\phi(14)=6$ vì từ 1 đến 13, ta có các số 1, 3, 5, 7, 9, 11, 13 là các số nguyên tố cùng nhau với 14. Và số lượng các số này = 6.

Nhưng thực ra, người ta đã chứng minh được một công thức đơn giản hơn để tìm $\phi(N)$, đó là:
$\phi(N)=(p-1)(q-1)$

Bước 4: Tìm số e. Số e là số được tìm sao cho thõa mãn:
1 < e <$\phi(N)$
e là số nguyên tố cùng nhau với N
e là số nguyên tố cùng nhau với $\phi(N)$
Vậy ta có e = 5

Bước 5: tìm d sao cho thõa mãn công thức:
$de(mod\phi(N))=1$
Theo công thức trên, ta có : d*5mod6=1 . Vậy ta có d có thể là 5, 11, 17 ... Ở đây mình chọn d = 11.

Vậy là ta đã tìm được 5 con số quan trọng của thuật toán RSA. Từ 5 con số này, ta sẽ tạo ra được 2 key. Một key dùng để mã hóa (encrypt key) và một key dùng để giải mã (decrypt key).
Encrypt key được tạo nên bởi cặp số (e,N) . Tức là encrypt key = (5,14). Decrypt key lại được tạo nên bởi cặp số (d,N). decrypt key = (11,14).

Việc dùng 2 cặp key này để mã hóa và giải mã như thế nào. Bạn hãy xem lại phần mã hóa bất đối xứng mình đã viết trong bài này nhé.

Tại sao RSA bảo mật?

Theo như cách hoạt động của encrypt key và decrypt key, thì 1 trong 2 sẽ là public key ( khái niệm public key và private key mình đã viết trong bài này). Có nghĩa là, hacker có thể biết được giá trị N.
Bạn hãy để ý, toàn bộ quá trình tạo nên encrypt key và decrypt key đều từ 2 con số q và p. Mà N=p*q. Vậy thì hacker chẳng phải dễ dàng đoán ra được p và q hay sao?????
Ví dụ N =14. Hacker có thể dễ dàng đoán ra được cặp số ngyên tố 2 và 7. Và từ đây, hacker có thể dễ dàng tái tạo được 2 key theo như 5 bước ở trên. Vậy tại sao RSA lại bảo mật được???

Lí do rất đơn giản. Trong thực tế, khi người ta chọn 2 số q và p, thì 2 số này là 2 con số cực kì lớn. Do đó, N cũng là 1 con số siêu to khổng lồ. Và để tìm ra được 2 số nhân với nhau để cho ra N, máy tính phải mất rất rất nhiều thời gian để có thể tìm ra được. Đây là lí do khiến cho hacker không thể truy ra được 2 key mặc dù đã có sẳn N.
Share:

Thứ Bảy, 22 tháng 2, 2020

Tìm hiểu về symmetric và asymmetric encryption

 Nếu là một lập trình viên, chắc hẳn bạn cũng từng nghe qua về mã hóa (encryption). Mã hóa là quá trình bạn chuyển 1 loại dữ liệu dễ đọc nào đó thành 1 loại dữ liệu đọc vào chả hiểu cái mẹ gì cả.

 Tại sao cần phải mã hóa dữ liệu? Lí do đơn giản là bạn không muốn người khác đọc được. Giống như tình huống bạn thích 1 đứa nào đó trong lớp và muốn gửi thư tình cho người đó. Nhưng để thư tình tới được tay người thương, thì phải được chuyển qua tay những đứa khác. Và để không ai ngoài người thương đọc được thư, thì bạn sẽ mã hóa đó bằng 1 cách nào đó. Và người thương của bạn cũng biết cách để giải mã, do đó sẽ chỉ có người đó đọc được nội dung thư mà thôi.

Symmetric Encryption ( mã hóa đối xứng )


Nếu như quá trình mã hóa và giải mã cùng sử dụng chung một key(chìa khóa), thì đây là symmetric encryption


Ví dụ: nội dung bạn muốn mã hóa là số 2. Bạn sẽ mã hóa bằng cách: lấy 2 công với 3, sau đó lấy kết quả nhân với 5.
Ta sẽ có (2+3)*5 = 25. Vậy số 2 đã được mã hóa thành số 25. Và key để mã hóa ở đây là (3,5).

Để giải mã, ta chỉ cần sử dụng lại key (3,5) để giải mã bằng cách: lấy 25 chia cho 5 và trừ đi 3: 25/5 -3 =2.

 Nhưng, với cách mã hóa như thế này nảy sinh ra 1 vấn đề. Hãy nghĩ tới trường hợp, bạn muốn nhận thông điệp từ 100 người khác nhau. Vậy để 100 người này mã hóa được nội dung trước khi gửi đi thì bạn phải gửi 100 cái key cho 100 người đó.

 Và việc có quá nhiều người giữ key sẽ dẫn đến việc thiếu bảo mật, vì có thể 1 người nào đó bị lộ key, thì người khác cũng có thể giải mã và đọc được nội dung từ 100 người này.

Vậy nên người ta mới nghĩ tới 1 loại mã hóa khác, đó là Asymmetric Encryption

Asymmetric Encryption (mã hóa bất đối xứng)

Asymmetric là khi quá trình mã hóa và giải mã dùng các key khác nhau


Giả sử ta có 1 key để mã hóa là : (5,14) . Nội dung để mã hóa là số 2. Để mã hóa, ta làm theo trình tự sau: lấy 2 mũ 5, và chia lấy dư cho 14.

Với cách mã hóa trên, ta đã có được nội dung sau khi mã hóa là : 4 . Nhưng nếu bạn cũng sử dụng key (5,14) để giải mã, thì sẽ không được. Vì với phép tính chia lấy dư thì không có cách nào để truy ngược lại được cả.

Để giải mã được theo cách này, ta phải dùng 1 chìa khóa khác. Chìa giải mã của chúng ta là (11,14). Và chúng ta sẽ giải mã theo trình tự sau: lấy 4 mũ 11 và chia lấy dư cho 14.

Kết quả giải mã của chúng ta là 2. Chính xác!!!
Và quay lại với bài toán trên, nếu bạn muốn nhận tin nhắn từ 100 người khác nhau, bạn chỉ cần gửi 100 chìa khóa mã hóa cho 100 người đó. Nếu như có ai ăn cắp được key mã hóa, họ cũng không thể giải mã. Vì chỉ có bạn mới là người cầm key giải mã.
Do đó, phương pháp asymmetric bảo mật hơn rất nhiều so với symmetric.

Vậy làm thế nào để tạo ra được 2 chìa khóa mã hóa và giải mã như trên. Rất may, chúng ta đã được thừa hướng những thuật toán vĩ đại được phát minh từ trước. Ví dụ những thuật toán như RSA (mình đã viết ở đây), DSA, ECC, Diffie Hellman... Để tạo ra được 2 chìa khóa kì diệu trên, chỉ cần áp dụng theo những thành tựu này là chúng ta sẽ thành công.

Share:

Thứ Bảy, 15 tháng 2, 2020

Tìm hiểu thuật toán Backtracking và ứng dụng giải Sudoku

 Mình để ý là trên blog mình viết khá nhiều thuật toán về AI, viết cả về máy học. Và như các bạn cũng thấy, những thuật toán này giải được những bài toán khá phức tạp, và do đó những thuật toán này cũng phức tạp không kém :)). Nhưng đã bao giờ bạn nghĩ chúng ta có thể giải những bài toán phức tạp bằng sức mạnh lưu trữ và tốc độ tính toán của máy tính chưa? Chúng ta sẽ không phải cất công suy nghĩ quá nhiều về thuật toán nữa. Máy tính sẽ lo hết cho chúng ta. ( Viết tới đây thì mình cũng thấy nó na ná như là máy học thật ^^)
 Hôm nay mình sẽ giới thiệu các bạn 1 thuật toán có tên là Backtracking, một thuật toán siêu đơn giản nhưng sức mạnh của nó thì vô cùng lớn.
Các ứng dụng mình viết trong bài này đều đã được host lên ở đây, các bạn có thể vào xem nhé.

Backtracking là gì?

Backtracking algorithm là kĩ thuật sử dụng các tổ hợp phép thử để giải quyết 1 vấn đề tính toán nào đó

Để dễ hiểu, ta đi vào ví dụ về mở khóa mã pin chẳng hạn. Ta có 1 mã pin có 4 chữ số từ 0-9. Để giải được mã pin này, thì có 1 cách đơn giản đó là phải thử nghiệm từng khả năng để xem có đúng không.

Đầu tiên ta có thể điền vào ô đầu là số 0.
Tiếp tục điền vào các ô tiếp theo.
Giả sử mã pin 0000 là mã pin sai. Vậy bước tiếp theo của backtracking là gì? Câu trả lời là thuật toán này sẽ thử khả năng tiếp theo của ô cuối cùng. Và ta sẽ có mã pin tiếp theo như sau
Và nếu mã pin 0001 cũng là mã pin sai. Chúng ta sẽ từ từ tăng giá trị hàng đơn vị lên 1 đơn vị. Ta sẽ có các mã pin từ 0002 đến 0009.
Vấn đề là các mã pin trên đều sai. Lúc này, backtracking sẽ quay lại ô trước hàng đơn vị. Đây cũng là lí do thuật toán này mang tên backtracking ( truy vết). Ta sẽ có mã pin được nhập như sau.
Lúc này, quá trình thử nghiệm lại tiếp tục với số hàng đơn vị, ta sẽ có các mã pin từ 0010 đến 0019. Và nếu những mã pin này sai, thì backtracking lại tiếp tục để cho ra các số 0020 đến 0029.
Cứ như thế, quá trình chạy sẽ kiểm thử các mã pin từ 0000 đến 9999. Và tất nhiên, khi mã pin đúng thì thuật toán sẽ kết thúc.

Ứng dụng giải sudoku bằng backtracking

Tương tự như việc giải mã pin, backtracking sẽ được chúng ta áp dụng để giải ma trận sudoku.
Chúng ta sẽ áp dụng kiểm thử từng khả năng của mỗi ô. Quá trình này sẽ kết thúc khi ta đạt được ma trận đúng của sudoku.

solve(maze){
        let currentCell = maze.getNextUnsolveCell();
        //already solved maze
        if(currentCell===undefined) {
            return true;
        }
        let availableValues = maze.getAvailableValues(currentCell);
        for(let value of availableValues){
            maze.setValueForCell(currentCell,value);
            if(this.solve(maze)){
                return true;
            }
        }
        maze.setValueForCell(currentCell,0);

        return false;
    }
Đoạn code trên chính là backtracking trong việc giải sudoku. Quá trình backtracking sẽ được thực hiện trong lập trình bởi đệ quy.
Các bước của thuật toán có thể được viết lại như sau:
Bước 1 : lấy ô chưa được giải tiếp theo để thử nghiệm
Bước 2: nếu không có ô nào để giải nữa thì ma trận đã giải xong
Bước 3: tìm những giá trị mà ô hiện tại có thể tồn tại. ( Thực ra bước này không cần thiết, bạn có thể cho các giá trị ứng thử từ 1 đến 9 cũng được. Mình thêm phần này  vào để tăng tốc độ tính toán lên thôi.)
Bước 4: với mỗi giá trị có thể ứng thử, ta ứng thử nó vào ô hiện tại.
Bước 5: thực hiện đệ quy lại từ Bước 1.
Bước 6: nếu đã thử hết tất cả giá trị ứng thử rồi mà không được thì reset lại ô hiện tại về 0
Các bạn để ý, thuật toán của chúng ta chẳng có gì là phức tạp cả. Nhưng nó đã giải quyết được 1 bài toán rất phức tạp là giải sudoku. Cái cốt lõi đằng sau việc này là chúng ta đã lợi dụng khả năng lưu trữ và tốc độ xử lý của máy tính để truy vết các phép thử.

Mình đã viết 1 ứng dụng để giải sudoku bằng javascript ( đoạn code demo ở trên đã thay đổi sau khi mình cập nhật hiệu ứng animation cho ứng dụng). Code các bạn có thể clone về ở github của mình.
Cám ơn các bạn đã ghé thăm và đọc bài trên blog của mình ^^
Share:

Chủ Nhật, 9 tháng 2, 2020

Thuật giải A* và ứng dụng tìm đường đi trong ma trận

 Hôm nay mình lại có nhã hứng làm 1 bài về AI. Và thứ mình muốn giới thiệu đó là thuật giải A*. Chúng ta sẽ đi tìm hiểu thuật giải A* này là gì và thử làm 1 cái app ứng dụng thuật giải này thử nhé.
Các ứng dụng mình viết trong bài này đều đã được host lên ở đây, các bạn có thể vào xem nhé.

Thuật giải A* là gì?

Thuật giải A* là thuật giải tìm đường đi ngắn nhất trong đồ thị. Đây là 1 giải thuật Heuristics, tức là đây là 1 giải thuật mang tính xấp xỉ, kết quả đầu ra có thể không hoàn toàn chính xác. Vậy câu hỏi đặt ra là: Tại sao không sử dụng một thuật giải tuyệt đối, kết quả đầu ra luôn luôn chính xác mà lại phải sử dụng thuật giải xấp xỉ như thế này?

Câu trả lời nằm ở chi phí tính toán. Để đưa ra được 1 kết quả chính xác, chúng ta phải vét cạn đồ thị ( nếu câu này sai thì mọi người hãy comment cho mình biết nhé), nhưng với thuật giải A*, nó không vét cạn, do đó, chi phí tính toán sẽ là thấp hơn.
Nói ra thì dài dòng, chốt lại là: Thuật giải A* là thuật giải tìm đường đi ngắn nhất trong đồ thị mà không vét cạn do đó mang tính xấp xỉ.

Thuật giải A* hoạt động như thế nào?

Trong thuật giải A*, mỗi 1 điểm trong đồ thị sẽ mang theo nó 3 giá trị cốt lõi:
   g: đây là chi phí cần phải bỏ ra để đi từ điểm bắt đầu đến điểm hiện tại.
   h: đây là chi phí ước lượng để đi từ điểm hiện tại đến điểm kết thúc.
   f : giá trị này thì dễ tính: f = g + h.
Dưới đây là thuật giải A*:
Input: 1 tập các điểm trong đồ thị với trọng số tương ứng. Node start và node end.
Output: Đường đi ngắn nhất từ node start đến node end.
Thuật giải gồm các bước sau đây:
B1: Khởi tạo 1 tập Open = rỗng . Khởi tạo tập Close = rỗng.
B2: Gán điểm Current = start để tính toán. Đưa điểm start vào tập Close.
B3: nếu current là end thì truy ngược lại đường đi dựa theo giá trị parent.
B4: Tìm tất cả những điểm lân cận (có thể đi được) từ điểm current và đưa vào 1 tập Neighbors. Duyệt qua từng điểm trong neighbors.
   B4.1: nếu điểm neighbor đang xét đã có trong tập Close => bỏ qua
             gán neighbor g = current g + cost(current, neighbor)                 
                    neighbor f = neighbor g + neighbor h.
                    neighbor parent = current
   B4.2: nếu điểm neighbor đang xet chưa có trong tập Open thì đưa neighbor vào tập Open
   B4.3: nếu neighbor đã có trong tập Open.
             Nếu neighbor trong open có g > neighbor g. Thì đưa neighbor vào thay thế cho neighbor trong open.
B5: Nếu Open còn phần tử thì lấy phần tử có f nhỏ nhất để làm current, đưa current vào tập close và quay lại bước 3.
B6: nếu Open rỗng thì không tìm được đường đi.

Chắc chắn đọc xong đoạn thuật giải trên các bạn chẳng hiểu được gì đâu. Vì vậy chúng ta  sẽ đi vào ví dụ cho dễ hiểu nhé:
Ta có đồ thị sau:
Điểm bắt đầu là A, điểm kết thúc là L. Với các giá trị h (ước lượng) của từng node tới L như sau:

POINT
h value
A
9
B
8
C
8
D
7
E
6
F
5
G
4
H
4
K
3
L
0
Bây giờ thì bắt đầu chạy thử thuật toán với ví dụ nhé:
Step
Open Set
Close Set
Current
Neighbors
Description
1
{}
{}
A(g=0, h=9, f=9)


2
{}
A(g=0, h=9, f=9)
A(g=0, h=9, f=9)
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
G(g=6,h=4,f=10,parent=A)

3
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
G(g=6,h=4,f=10,parent=A)
A(g=0, h=9, f=9)


Vì trong tập Open, G có f bé nhất nên lấy làm current.
4
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
G(g=6,h=4,f=10,parent=A)
C(g=8,h=8,f=16,parent=G)
K(g=9,h=3,f=12,parent=G)

5
B(g=3,h=8,f=11, parent=A)
C(g=7,h=8,f=15, parent=A)
K(g=9,h=3,f=12,parent=G)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)


Vì trong tập open, B có f nhỏ nhất nên lất làm current
6
C(g=7,h=8,f=15, parent=A)
K(g=9,h=3,f=12,parent=G)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)

B(g=3,h=8,f=11, parent=A)

D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)

7
C(g=7,h=8,f=15, parent=A)
K(g=9,h=3,f=12,parent=G)
D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)



Vì trong tập open, K có f nhỏ nhất nên lất làm current
8
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
K(g=9,h=3,f=12,parent=G)
D(g=12,h=7,f=19,parent=K)
L(g=16,h=0,f=16,parent=K)

9
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
E(g=7,h=6,f=13,parent=B)
L(g=16,h=0,f=16,parent=K)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)


Vì trong tập open, E có f nhỏ nhất nên lất làm current
10
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=16,h=0,f=16,parent=K)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)

E(g=7,h=6,f=13,parent=B)

F(g=8,h=5,f=13,parent=E)
H(g=15,h=4,f=19,parent=E)

11
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=16,h=0,f=16,parent=K)
F(g=8,h=5,f=13,parent=E)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)



Vì trong tập open, F có f nhỏ nhất nên lất làm current
12
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=16,h=0,f=16,parent=K)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)
F(g=8,h=5,f=13,parent=E)
F(g=8,h=5,f=13,parent=E)

L(g=10,h=0,f=10,parent=F)
Vì L trong neighbor có g < L trong open, nên thay L trong open bằng L trong neighbor
13
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
L(g=10,h=0,f=10,parent=F)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)
F(g=8,h=5,f=13,parent=E)


Vì trong tập open, L có f nhỏ nhất nên lất làm current
14
C(g=7,h=8,f=15, parent=A)
D(g=8,h=7,f=15,parent=B)
H(g=15,h=4,f=19,parent=E)
A(g=0, h=9, f=9)
G(g=6,h=4,f=10,parent=A)
B(g=3,h=8,f=11, parent=A)
K(g=9,h=3,f=12,parent=G)
E(g=7,h=6,f=13,parent=B)
F(g=8,h=5,f=13,parent=E)
L(g=10,h=0,f=10,parent=F)
L(g=10,h=0,f=10,parent=F)

Vì L là điểm end. Nên thuật toán kết thúc. Truy ngược đường đi theo parent ta có:
A -> B -> E -> F -> L

Nhìn vào cách chạy thuật toán trong bảng trên, mình mong là các bạn đã hiểu được thuật giải A* hoạt động như thế nào.
Và cũng trong một khoảng thời gian rảnh rỗi và có hứng thú, mình cũng đã viết 1 đoạn chương trình bằng javascript hiện thực hóa thuật giải A* này trong việc tìm đường đi trong ma trận.

Code mình đã up sẵn lên github ở đây. Bạn chỉ cần clone về và chạy thôi nhé.
Cám ơn các bạn đã xem bài viết này của mình. Nếu có thắc mắc gì thì cứ comment vào blog. Nếu được mình sẽ giải đáp.
Share:
Được tạo bởi Blogger.