Chương 14 : Mậtmã(Cryptography)
Mật mã (cryptography) là một trong những mặt phức tạp nhất của quá trình phát triển
phần mềm mà bất kỳ nhà phát triển nào cũng sẽ sử dụng. Lý thuyết kỹ thuật mậtmã hiện
đại cực kỳ khó hiểu và đòi hỏi một mức kiến thức toán học mà tương đối ít người có
được. May mắn là thư viện lớp .NET Framework cung cấp các hiện thự
c dễ sử dụng cho
hầu hết các kỹ thuật mậtmã thông dụng và hỗ trợ các giải thuật phổ biến nhất. Chương
này sẽ bàn về các vấn đề sau:
Tạo số ngẫu nhiên (mục 14.1).
Tạo và xác minh các mã băm mậtmã và các mã băm có khóa (mục 14.2, 14.3, 14.4,
và 14.5).
Sử dụng giải thuật đối xứng và không đối xứng để mật hóa và giải mật hóa dữ liệu
(mục 14.6 và 14.8).
Tìm lại, lưu trữ, và chuyển đổi các khóa mậtmã (mục 14.7, 14.9, và 14.10).
# Khi nghĩ cách áp dụng các kỹ thuật trong chương này vào mã lệnh, bạn nên
nhớ rằng mậtmã chẳng phải là cái mà bạn hiện thực đơn lẻ. Mậtmã không
ngang bằng với bảo mật (security); sử dụng mậtmã chỉ là một phần nhỏ trong
việc tạo một giải pháp an toàn.
Đối với những ai chưa quen thuộc với mật mã, dưới đây là định nghĩa của một số từ
quan
trọng:
• Encrypt (động từ, tạm dịch là mật hóa) là mã hóa thông tin theo cách nào đó để mọi
người không thể đọc được nó, trừ những ai có khóa.
• Decrypt (động từ, tạm dịch là giải mật hóa) là giải mã thông tin đã-được-mật-hóa.
• Key là chuỗi các bit dùng để mật hóa và giải mật hóa thông tin.
• Plaintext là text chưa-được-mật-hóa hay đã-được-giải-mật-hóa.
• Ciphertext là text đã-được-m
ật-hóa.
1.1 Tạo số ngẫu nhiên
V
V
Bạn cần tạo một số ngẫu nhiên dùng cho các ứng dụng mậtmã và bảo mật.
#
#
Sử dụng một bộ tạo số ngẫu nhiên mậtmã (cryptographic random number
generator), chẳng hạn
System.Security.Cryptography.RNGCryptoServiceProvider.
Lớp System.Random là một bộ tạo số giả ngẫu nhiên, nó sử dụng một giải thuật toán học
để mô phỏng việc tạo số ngẫu nhiên. Thực ra, giải thuật này là tất định (deterministic),
nghĩa là bạn luôn có thể tính được số kế tiếp sẽ là gì dựa trên số đã được t
ạo trước đó.
Điều này nghĩa là các số được tạo bởi lớp Random sẽ không phù hợp khi tính bảo mật
được ưu tiên, chẳng hạn tạo khóa mật hóa và password.
Khi cần một số ngẫu nhiên không tất định (nondeterministic) dùng trong các ứng dụng
liên quan đến mậtmã hay bảo mật, bạn phải sử dụng bộ tạo số ngẫu nhiên dẫn xuất từ lớp
System.Security.Cryptography.RandomNumberGenerator. Đây là một lớp trừu tượng mà
t
ất cả các bộ tạo số ngẫu nhiên cụ thể đều sẽ thừa kế từ nó. Hiện tại, chỉ có một hiện thực
là lớp RNGCryptoServiceProvider. Lớp này cung cấp một vỏ bọc được-quản-lý cho hàm
CryptGenRandom của Win32 CryptoAPI, và bạn có thể sử dụng để đổ vào một mảng
byte các giá trị byte ngẫu nhiên.
# Các số do RNGCryptoServiceProvider sinh ra không thật sự ngẫu nhiên. Tuy
nhiên, chúng “đủ” ngẫu nhiên để đáp ứng yêu cầu cho các ứng dụng mậtmã và
bảo mật trong hầu hết các môi trường chính phủ và thương mại.
Lớp cơ sở RandomNumberGenerator là một factory cho các lớp hiện thực dẫn xuất từ đó.
Gọi
RandomNumberGenerator.Create("System.Security.Cryptography.RNGCryptoServicePr
ovider") sẽ trả về một thể hiện của RNGCryptoServiceProvider, và bạn có thể sử dụng nó
để tạ
o số ngẫu nhiên. Ngoài ra, vì RNGCryptoServiceProvider là hiện thực duy nhất nên
nó sẽ là lớp mặc định được tạo ra khi bạn gọi phương thức Create không có đối số:
RandomNumberGenerator.Create().
Ví dụ dưới đây tạo một đối tượng RNGCryptoServiceProvider và sử dụng nó để tạo các
giá trị ngẫu nhiên. Phương thức GetBytes đổ vào một mảng byte các giá trị byte ngẫu
nhiên. Bạn có thể sử dụng phương thức GetNonZeroBytes nếu cần dữ liệu ng
ẫu nhiên
không chứa giá trị zero.
using System;
using System.Security.Cryptography;
public class SecureRandomNumberExample {
public static void Main() {
// Tạo mảng byte dùng để lưu trữ dữ liệu ngẫu nhiên.
byte[] number = new byte[32];
// Tạo bộ tạo số ngẫu nhiên mặc định.
RandomNumberGenerator rng = RandomNumberGenerator.Create();
// Tạo dữ liệu ngẫu nhiên.
rng.GetBytes(number);
// Hiển thị dữ liệu ngẫu nhiên.
Console.WriteLine(BitConverter.ToString(number));
}
}
# Những nỗ lực tính toán cần thiết để tạo một số ngẫu nhiên với
RNGCryptoServiceProvider lớn hơn nhiều so với Random. Đối với mục đích
thường ngày, sử dụng RNGCryptoServiceProvider là quá mức cần thiết. Bạn
nên xem xét số lượng số ngẫu nhiên cần tạo và mục đích của các số này trước
khi quyết định sử dụng RNGCryptoServiceProvider. Sử dụng lớp
RNGCryptoServiceProvider quá mức và không cần thiết có th
ể ảnh hưởng
đáng kể lên hiệu năng của ứng dụng.
1.2 Tính mã băm của password
V
V
Bạn cần lưu trữ password của người dùng một cách an toàn để bạn có thể sử
dụng nó để xác thực người dùng đó trong tương lai.
#
#
Đừng lưu trữ password của người dùng ở dạng plaintext vì đây là một nguy cơ
bảo mật lớn. Thay vào đó, hãy tạo và lưu trữ một mã băm của password bằng
một lớp giải thuật băm dẫn xuất từ lớp
System.Security.Cryptography.HashAlgorithm. Khi xác thực, tạo mã băm của
password và so sánh nó với mã băm đã được lưu trữ.
Các giải thuật băm là các hàm mậtmã một chiều, nhận plaintext có chiều dài thay
đổi và
tạo một giá trị số có kích thước cố định. Chúng là một chiều vì gần như không thể tìm lại
plaintext gốc từ mã băm. Các giải thuật băm là tất định (deterministic); áp dụng cùng giải
thuật băm cho một mẩu plaintext luôn tạo ra cùng mã băm. Điều này khiến mã băm trở
nên hữu ích cho việc xác định hai khối plaintext (trong trường hợp này là password) có
giống nhau hay không. Mục đích của các giải thuật b
ăm bảo đảm rằng—mặc dù không
phải không xảy ra—khả năng hai mẩu plaintext khác nhau tạo ra cùng mã băm là cực kỳ
nhỏ. Ngoài ra, không có mối tương quan nào giữa sự giống nhau của hai mẩu plaintext và
mã băm của chúng; một khác biệt nhỏ trong plaintext cũng có thể gây ra khác biệt đáng
kể trong mã băm.
Khi sử dụng password để xác thực một người dùng, bạn không quan tâm đến nội dung
của password do người dùng nhập vào. Bạn chỉ cầ
n biết rằng password được nhập trùng
khớp với password mà bạn đã ghi lại cho người dùng đó trong cơ sở dữ liệu tài khoản.
Bản chất của các giải thuật băm khiến chúng trở nên lý tưởng trong việc lưu trữ password
một cách an toàn. Khi người dùng cung cấp một password mới, bạn phải tạo mã băm của
password và lưu trữ mã băm này, rồi loại bỏ password dạng text. Mỗi khi người dùng xác
thực với ứng dụng của bạn, tính mã băm của password do người đó cung cấp và so sánh
nó với mã băm mà bạn đã lưu trữ.
# Người ta thường hỏi cách thu lấy password từ một mã băm. Và câu trả lời là
không thể. Mục đích của mã băm là đóng vai trò như một token và bạn có thể
tùy ý lưu trữ nó mà không sinh ra lỗ hổng bảo mật nào. Nếu người dùng quên
password, bạn không thể tìm lại nó từ mã băm đã được lưu trữ; bạn phải reset
tài khoản này thành giá trị mặc định nào đó, hoặc tạo một password mới cho
ngườ
i dùng.
Lớp trừu tượng HashAlgorithm cung cấp lớp cơ sở để tất cả các hiện thực giải thuật băm
cụ thể dẫn xuất từ đó. Thư viện lớp .NET Framework có sáu hiện thực giải thuật băm cụ
thể (được liệt kê trong bảng 14.1), mỗi lớp hiện thực là một thành viên của không gian
tên System.Security.Cryptography. Các lớp với phần đuôi là CryptoServiceProvider bọc
lấy các chứ
c năng do Win32 CryptoAPI cung cấp, trong khi các lớp với phần đuôi là
Managed được hiện thực hoàn toàn bằng mã lệnh được-quản-lý.
Bảng 14.1 Các hiện thực giải thuật băm
Tên giải thuật Tên lớp
Kích thước mã băm
(bit)
MD5
MD5CryptoServiceProvider 128
SHA hay SHA1 SHA1CryptoServiceProvider 160
SHA1Managed
SHA1Managed 160
SHA256 hay SHA-
256
SHA256Managed 256
SHA384 hay SHA-
384
SHA384Managed 384
SHA512 hay SHA-
512
SHA512Managed 512
Mặc dù bạn có thể trực tiếp tạo ra thể hiện của các lớp giải thuật băm, lớp cơ sở
HashAlgorithm là một factory cho các lớp hiện thực dẫn xuất từ nó. Gọi phương thức
tĩnh HashAlgorithm.Create với đối số là tên giải thuật sẽ trả về một đối tượng thuộc kiểu
đã được chỉ định. Sử dụng factory cho phép bạ
n ghi mã lệnh tổng quát và mã lệnh này có
thể làm việc với bất kỳ hiện thực giải thuật băm nào.
Một khi bạn đã có đối tượng HashAlgorithm, phương thức ComputeHash của nó nhận
một mảng byte chứa plaintext và trả về một mảng byte mới chứa mã băm được tạo ra.
Bảng 14.1 cho biết kích thước của mã băm (tính bằng bit) được tạo ra bởi mỗi lớp giải
thuật băm.
Lớp HashPasswordExample dưới đây trình bày cách tạo mã băm từ một chuỗi (password
chẳng hạn). Ứng dụng này cần hai đối số dòng lệnh: tên của giải thuật băm cần sử dụng
và chuỗi cần tạo mã băm. Vì phương thức HashAlgorithm.ComputeHash yêu cầu một
mảng byte nên trước hế
t bạn phải mã hóa chuỗi nhập bằng lớp System.Text.Encoding
(lớp này cung cấp các cơ chế dùng để chuyển chuỗi thành/từ các định dạng mã hóa ký tự
khác nhau).
using System;
using System.Text;
using System.Security.Cryptography;
public class HashPasswordExample {
public static void Main(string[] args) {
// Tạo HashAlgorithm của kiểu được chỉ định bởi
// đối số dòng lệnh thứ nhất.
using (HashAlgorithm hashAlg = HashAlgorithm.Create(args[0])) {
// Chuyển chuỗi password (đối số dòng lệnh thứ hai)
// thành một mảng byte.
byte[] pwordData = Encoding.Default.GetBytes(args[1]);
// Tạo mã băm của password.
byte[] hash = hashAlg.ComputeHash(pwordData);
// Hiển thị mã băm của password.
Console.WriteLine(BitConverter.ToString(hash));
}
}
}
Chạy lệnh HashPasswordExample SHA1 ThisIsMyPassword sẽ hiển thị mã băm sau
đây:
80-36-31-2F-EA-D9-93-45-79-34-C9-FD-21-EE-8D-05-16-DC-A1-E2
1.3 Tính mã băm của file
V
V
Bạn cần xác định nội dung của một file có thay đổi theo thời gian hay không.
#
#
Tạo mã băm cho nội dung của file bằng phương thức ComputeHash của lớp
System.Security.Cryptography.HashAlgorithm. Lưu trữ mã băm này để sau
này so sánh với các mã băm được tạo mới.
Ngoài việc cho phép bạn lưu trữ password một cách an toàn (đã được thảo luận trong
mục 14.2), mã băm còn cung cấp một phương cách rất hay để xác định một file có thay
đổi hay không. Bằng cách tính toán và lưu trữ mã băm của một file, sau này bạn có thể
tính lại mã băm củ
a file này để xác định file có thay đổi trong thời gian chuyển tiếp hay
không. Giải thuật băm sẽ sinh ra một mã băm rất khác ngay cả chỉ với một thay đổi rất
nhỏ trong file, nên khả năng hai file khác nhau cho ra cùng mã băm là cực kỳ nhỏ.
# Các mã băm chuẩn không phù hợp khi gửi cùng với một file để bảo đảm tính
toàn vẹn của nội dung file. Nếu ai đó chặn được file trên đường đi, người này
có thể dễ dàng thay đổi file và tính lại mã băm. Chúng ta sẽ thảo luận một biến
thể của mã băm trong mục 14.5 (mã băm có khóa), mã băm này phù hợp cho
việc bảo đảm tính toàn vẹn của file trên đường đi.
Dễ dàng tạo được mã băm củ
a một file với lớp HashAlgorithm. Trước hết, thể hiện hóa
một trong các hiện thực giải thuật băm dẫn xuất từ lớp HashAlgorithm (bạn cần truyền
tên giải thuật băm cho phương thức HashAlgorithm.Create—xem tên các giải thuật băm
hợp lệ trong bảng 14.1). Kế tiếp, thay vì truyền một mảng byte cho phương thức
ComputeHash, bạn hãy truyền một đối tượng System.IO.Stream mô tả file cần được tạo
mã bă
m. Đối tượng HashAlgorithm xử lý quá trình đọc dữ liệu từ Stream và trả về một
mảng byte chứa mã băm cho file.
Lớp HashStreamExample dưới đây trình bày cách tạo mã băm từ một file. Bạn phải chỉ
định tên giải thuật băm và tên file làm đối số dòng lệnh, ví dụ HashStreamExample
SHA1 HashStreamExample.cs.
using System;
using System.IO;
using System.Security.Cryptography;
public class HashStreamExample {
public static void Main(string[] args) {
// Tạo một HashAlgorithm với kiểu được chỉ định trong
// đối số dòng lệnh thứ nhất.
using (HashAlgorithm hashAlg = HashAlgorithm.Create(args[0])) {
// Mở một FileStream cho file được chỉ định trong
// đối số dòng lệnh thứ hai.
using (Stream file = new FileStream(args[1],
FileMode.Open)) {
// Tạo mã băm cho nội dung của file.
byte[] hash = hashAlg.ComputeHash(file);
// Hiển thị mã băm.
Console.WriteLine(BitConverter.ToString(hash));
}
}
}
}
1.4 Kiểm tra mã băm
V
V
Bạn cần xác minh một password hoặc xác nhận một file vẫn không thay đổi
bằng cách so sánh hai mã băm.
#
#
Chuyển cả mã băm cũ và mới thành chuỗi thập lục phân, chuỗi Base64, hay
mảng byte và so sánh chúng.
Bạn có thể sử dụng mã băm để xác định hai mẩu dữ liệu có giống nhau hay không, để
không phải lưu trữ hay duy trì việc truy xuất đến dữ liệu gốc. Để xác định dữ liệu có thay
đổi theo thời gian hay không, bạn phải tạo và lưu trữ mã băm của dữ liệu gốc. Sau đ
ó,
hãy tạo một mã băm khác cho dữ liệu này rồi so sánh mã băm cũ và mới để cho thấy có
thay đổi nào xảy ra hay không. Định dạng của mã băm gốc sẽ xác định cách thức phù hợp
nhất để kiểm tra mã băm mới được tạo.
# Nhiều mục trong chương này sử dụng phương thức ToString của lớp
System.BitConverter để chuyển mảng byte thành giá trị chuỗi thập lục phân
khi hiển thị. Mặc dù dễ sử dụng và thích hợp cho mục đích hiển thị, bạn có thể
nhận thấy cách này không phù hợp khi lưu trữ mã băm vì nó đặt dấu gạch nối
(-) giữa mỗi giá trị byte (ví dụ, 4D-79-3A-C9-…). Ngoài ra, lớp BitConverter
không cung cấp phương thức nào để
phân tích một biểu diễn chuỗi như thế trở
về một mảng byte.
Mã băm thường được lưu trữ trong file text ở dạng chuỗi thập lục phân (ví dụ,
89D22213170A9CFF09A392F00E2C6C4EDC1B0EF9) hoặc chuỗi được mã hóa theo
Base64 (ví dụ, idIiExcKnP8Jo5LwDixsTtwbDvk=). Mã băm cũng có thể được lưu trữ
trong cơ sở dữ liệu ở dạng giá trị byte thô. Bất kể bạn lưu trữ mã băm theo cách nào,
bước đầu tiên khi so sánh mã b
ăm cũ và mới là đưa chúng về một dạng chung.
Đoạn mã dưới đây chuyển mã băm mới (mảng byte) thành chuỗi thập lục phân khi so
sánh với mã băm cũ. Ngoài phương thức BitConverter.ToString mà chúng ta đã thảo luận
ở trên, thư viện lớp .NET Framework không cung cấp phương thức nào để chuyển một
mảng byte thành chuỗi thập lục phân. Bạn phải viết một vòng lặp đi qua các phần tử của
mảng byte, chuyển mỗi byte thành chuỗi, và gắn chuỗi này vào biểu diễn chuỗi thập lục
phân của mã băm. Sử dụng System.Text.StringBuilder sẽ tránh tạo ra các chuỗi mới
không cần thiết mỗi khi vòng lặp gắn giá trị
byte kế tiếp vào chuỗi kết quả (xem mục 2.1
để biết thêm chi tiết).
// Phương thức dùng để so sánh mã băm mới với
// mã băm có sẵn (được biểu diễn ở dạng chuỗi thập lục phân).
private static bool VerifyHexHash(byte[] hash, string oldHashString) {
// Tạo biểu diễn chuỗi cho mã băm mới.
System.Text.StringBuilder newHashString =
new System.Text.StringBuilder(hash.Length);
foreach (byte b in hash) {
newHashString.AppendFormat("{0:X2}", b);
}
// So sánh biểu diễn chuỗi của mã băm cũ và mới,
// và trả về kết quả.
return (oldHashString == newHashString.ToString());
}
Trong đoạn mã dưới đây, mã băm mới là một mảng byte và mã băm cũ là một chuỗi được
mã hóa theo Base64. Đoạn mã này sẽ mã hóa mã băm mới thành chuỗi Base64 rồi thực
hiện phép so sánh chuỗi.
// Phương thức dùng để so sánh mã băm mới với
// mã băm có sẵn (được biểu diễn ở dạng chuỗi Base64).
private static bool VerifyB64Hash(byte[] hash, string oldHashString) {
// Tạo biểu diễn chuỗi Base64 cho mã băm mới.
string newHashString = System.Convert.ToBase64String(hash);
// So sánh biểu diễn chuỗi của mã băm cũ và mới,
// rồi trả về kết quả.
return (oldHashString == newHashString);
}
Cuối cùng, đoạn mã dưới đây so sánh hai mã băm được biểu diễn ở dạng mảng byte. Thư
viện lớp .NET Framework không có phương thức nào thực hiện kiểu so sánh này, do đó
bạn phải viết một vòng lặp để so sánh các phần tử của hai mảng. Đoạn mã này có sử dụng
một vài kỹ thuật không tốn nhiều thời gian như: bảo đảm các mảng byte có cùng chiều
dài trước khi bắt đầu so sánh chúng, và trả về false khi tìm thấy khác biệt đầu tiên.
// Phương thức dùng để so sánh mã băm mới với
// mã băm có sẵn (được biểu diễn ở dạng mảng byte).
private static bool VerifyByteHash(byte[] hash, byte[] oldHash) {
// Nếu một mảng là null, hoặc hai mảng có chiều dài khác nhau
// thì chúng không bằng nhau.
if (hash == null || oldHash == null || hash.Length != oldHash.Length)
return false;
// Duyệt qua mảng byte và so sánh mỗi giá trị byte.
for (int count = 0; count < hash.Length; count++) {
if (hash[count] != oldHash[count]) return false;
}
// Hai mã băm bằng nhau.
return true;
}
. các mã băm mật mã và các mã băm có khóa (mục 14 .2, 14 .3, 14 .4,
và 14 .5).
Sử dụng giải thuật đối xứng và không đối xứng để mật hóa và giải mật hóa dữ liệu. hóa và giải mật hóa dữ liệu
(mục 14 .6 và 14 .8).
Tìm lại, lưu trữ, và chuyển đổi các khóa mật mã (mục 14 .7, 14 .9, và 14 .10 ).
# Khi nghĩ cách áp dụng