2010-12-01 54 views
40

Tôi đang viết mẫu đăng ký cho một ứng dụng nhưng vẫn gặp sự cố khi mới sử dụng C#.Hashing mật khẩu bằng MD5 hoặc sha-256 C#

Tôi đang tìm cách mã hóa/băm mật khẩu thành md5 hoặc sha-256, tốt nhất là sha-256.

Bất kỳ ví dụ hay nào? Tôi muốn nó có thể lấy thông tin từ "mật khẩu chuỗi;" và sau đó băm nó và lưu trữ trong biến "chuỗi hPassword;". Bất kỳ ý tưởng?

+4

Bạn sẽ làm gì với mật khẩu băm? Lưu trữ nó trong một cơ sở dữ liệu? Sau đó, chỉ cần băm nhỏ là không đủ ([Bảng cầu vồng] (http://en.wikipedia.org/wiki/Rainbow_table)). Sử dụng muối. – dtb

+2

Tôi sẽ lưu trữ nó trong cơ sở dữ liệu. Bạn đề xuất món gì. – Sean

+1

Mọi người hãy lưu ý rằng các câu trả lời dưới đây sẽ hết hạn. Và hãy nhớ rằng bạn không bao giờ nên sử dụng MD5 cho mật khẩu băm nữa. Đó là cũ, bị hỏng, và lỗi thời. Giải pháp dễ dàng, cập nhật và an toàn nhất cho hầu hết mọi người sẽ là [bcrypt] (https://bcrypt.codeplex.com/). Ít nhất là khi bình luận này được viết :) Argon2 có lẽ sẽ là đề xuất chuẩn sớm thôi. – Sammi

Trả lời

47

Bạn sẽ muốn sử dụng không gian tên System.Security.Cryptography; cụ thể là MD5 class hoặc SHA256 class.

Vẽ một chút từ mã trên this page, và với sự hiểu biết rằng cả hai lớp có lớp cơ sở tương tự (HashAlgorithm), bạn có thể sử dụng một chức năng như thế này:

public string ComputeHash(string input, HashAlgorithm algorithm) 
{ 
    Byte[] inputBytes = Encoding.UTF8.GetBytes(input); 

    Byte[] hashedBytes = algorithm.ComputeHash(inputBytes); 

    return BitConverter.ToString(hashedBytes); 
} 

Sau đó, bạn có thể gọi nó như thế này (đối với MD5):

string hPassword = ComputeHash(password, new MD5CryptoServiceProvider()); 

Hoặc cho SHA256:

string hPassword = ComputeHash(password, new SHA256CryptoServiceProvider()); 

Chỉnh sửa: Thêm hỗ trợ Salt
Như DTB chỉ ra trong các ý kiến, mã này sẽ mạnh hơn nếu nó bao gồm khả năng thêm salt. Nếu bạn không quen với nó, muối là một tập hợp các bit ngẫu nhiên được bao gồm như là một đầu vào cho hàm băm, đi một chặng đường dài để ngăn chặn các cuộc tấn công từ điển chống lại mật khẩu băm (ví dụ: sử dụng rainbow table). Đây là phiên bản sửa đổi của hàm ComputeHash hỗ trợ muối:

Hy vọng điều này hữu ích!

+0

OP có một chuỗi và muốn băm của chuỗi đó dưới dạng chuỗi. Các lớp mật mã chỉ đối phó với các mảng byte. – dtb

+0

'ASCIIEncoding.Default' gây nhầm lẫn trả về mã hóa UTF-16. Tôi đề nghị 'Encoding.UTF8' (ASCII sẽ quá hạn chế). – dtb

+1

Nếu bây giờ bạn cũng thêm tham số 'byte [] salt' vào phương thức' ComputeHash' và thêm các byte đó vào 'inputBytes' thì tôi chính thức yêu câu trả lời của bạn :-) – dtb

2

Nếu bạn đang đi để được lưu trữ các mật khẩu băm, sử dụng bcrypt thay vì SHA-256. Vấn đề là SHA-256 được tối ưu hóa cho tốc độ, điều này khiến cho việc tấn công brute force trên mật khẩu trở nên dễ dàng hơn nếu ai đó truy cập vào cơ sở dữ liệu của bạn.

Đọc bài viết này: Enough With The Rainbow Tables: What You Need To Know About Secure Password Schemes và điều này answer cho câu hỏi SO trước đó.

Một số trích dẫn từ bài viết:

Vấn đề là MD5 là nhanh. Vì vậy, các đối thủ cạnh tranh hiện đại của nó, như SHA1 và SHA256. Tốc độ là một mục tiêu thiết kế của một băm an toàn hiện đại, vì băm là một khối xây dựng của hầu hết các hệ thống mật mã, và thường được yêu cầu thực hiện trên cơ sở mỗi gói hoặc mỗi thông điệp.

Tốc độ chính xác là những gì bạn không muốn trong hàm băm mật khẩu.


Cuối cùng, chúng tôi biết rằng nếu chúng ta muốn lưu trữ mật khẩu một cách an toàn, chúng tôi có ba tùy chọn hợp lý: chương trình PHK của MD5, chương trình bcrypt Provos-Maziere, và SRP. Chúng tôi đã học được rằng sự lựa chọn đúng là Bcrypt.

+0

Các mật khẩu sẽ được lưu trữ trong cơ sở dữ liệu sau khi băm. Không được lưu trữ cục bộ. – Sean

+2

Ok, sau đó mọi mật khẩu sẽ được băm với một muối duy nhất và bạn nên sử dụng bcrypt, hoặc một cái gì đó giống như nó. Hashing với một muối dừng các cuộc tấn công bằng cách sử dụng một bảng cầu vồng, và sử dụng một thuật toán chậm hơn làm tăng nỗ lực cho một cuộc tấn công bạo lực trên mật khẩu băm. –

+0

Tôi sẽ không khuyên bạn nên sử dụng bcrypt trong .NET, StackOverflow bị hủy bcrypt để sử dụng PBKDF2. Xem các nhận xét từ Kevin Montrose trên blog StackOverflow http://blog.stackoverflow.com/2011/05/stack-exchange-is-an-openid-provider/ –

75

Không sử dụng băm đơn giản hoặc thậm chí là hàm băm muối. Sử dụng một số loại kỹ thuật tăng cường khóa như bcrypt (với số .NET implementation here) hoặc PBKDF2 (với built-in implementation).

Đây là ví dụ sử dụng PBKDF2.

Để tạo ra một chìa khóa từ mật khẩu của bạn ...

string password = GetPasswordFromUserInput(); 

// specify that we want to randomly generate a 20-byte salt 
using (var deriveBytes = new Rfc2898DeriveBytes(password, 20)) 
{ 
    byte[] salt = deriveBytes.Salt; 
    byte[] key = deriveBytes.GetBytes(20); // derive a 20-byte key 

    // save salt and key to database 
} 

Và sau đó để kiểm tra nếu một mật khẩu có giá trị ...

string password = GetPasswordFromUserInput(); 

byte[] salt, key; 
// load salt and key from database 

using (var deriveBytes = new Rfc2898DeriveBytes(password, salt)) 
{ 
    byte[] newKey = deriveBytes.GetBytes(20); // derive a 20-byte key 

    if (!newKey.SequenceEqual(key)) 
     throw new InvalidOperationException("Password is invalid!"); 
} 
+3

Tôi giả định trong bài đăng của bạn, nơi bạn nói ** của ** PBKDF2, bạn định nói ** HOẶC **. Tôi sẽ không khuyên bạn nên sử dụng bcrypt trong .NET, StackOverflow bị bỏ rơi bcrypt để sử dụng PBKDF2. Xem các nhận xét từ Kevin Montrose trên blog StackOverflow http://blog.stackoverflow.com/2011/05/stack-exchange-is-an-openid-provider/ –

+1

@Chris: Vâng, ý tôi là "hoặc PBKDF2"; đã cập nhật. Cá nhân tôi cũng đi cho PBKDF2, mặc dù tôi không biết bất kỳ lý do mạnh mẽ nào không sử dụng bcrypt. (Tôi đã từng nghe những điều tốt đẹp nói về nó.) – LukeH

+2

Vấn đề với bcrypt là không có .NET thực hiện nó đã được xác minh, đây là lý do tại sao StackOverflow chọn để thả việc sử dụng bcrypt của họ. Họ không muốn chi tiêu các nguồn lực để thực hiện xác minh trong khi triển khai thực hiện bản địa PBKDF2 trong .NET đã được xác minh cho Microsoft rồi. –

5

Bạn luôn nên muối mật khẩu trước khi băm khi lưu trữ chúng trong cơ sở dữ liệu.

cột cơ sở dữ liệu Khuyến cáo:

  • PasswordSalt: int
  • PasswordHash: nhị phân (20)

Hầu hết các bạn tìm thấy trực tuyến sẽ nói về ASCII mã hóa các muối và băm bài viết, nhưng điều đó không cần thiết và chỉ thêm tính toán không cần thiết. Ngoài ra nếu bạn sử dụng SHA-1, thì đầu ra sẽ chỉ là 20 byte để trường băm của bạn trong cơ sở dữ liệu chỉ cần dài 20 byte. Tôi hiểu yêu cầu của bạn về SHA-256, nhưng trừ khi bạn có lý do thuyết phục, việc sử dụng SHA-1 với giá trị muối sẽ là đủ trong hầu hết các hoạt động kinh doanh. Nếu bạn nhấn mạnh vào SHA-256, thì trường băm trong cơ sở dữ liệu cần phải dài 32 byte.

Dưới đây là một số chức năng sẽ tạo ra muối, tính toán hàm băm và xác minh hàm băm trên mật khẩu.

Hàm muối dưới đây tạo ra một muối mạnh mã hóa như một số nguyên từ 4 byte ngẫu nhiên được mã hóa tạo ra.

private int GenerateSaltForPassword() 
{ 
    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); 
    byte[] saltBytes = new byte[4]; 
    rng.GetNonZeroBytes(saltBytes); 
    return (((int)saltBytes[0]) << 24) + (((int)saltBytes[1]) << 16) + (((int)saltBytes[2]) << 8) + ((int)saltBytes[3]); 
} 

Mật khẩu sau đó có thể được băm bằng cách sử dụng hàm muối bên dưới. Muối được nối với mật khẩu và sau đó băm được tính toán.


private byte[] ComputePasswordHash(string password, int salt) 
{ 
    byte[] saltBytes = new byte[4]; 
    saltBytes[0] = (byte)(salt >> 24); 
    saltBytes[1] = (byte)(salt >> 16); 
    saltBytes[2] = (byte)(salt >> 8); 
    saltBytes[3] = (byte)(salt); 

    byte[] passwordBytes = UTF8Encoding.UTF8.GetBytes(password); 

    byte[] preHashed = new byte[saltBytes.Length + passwordBytes.Length]; 
    System.Buffer.BlockCopy(passwordBytes, 0, preHashed, 0, passwordBytes.Length); 
    System.Buffer.BlockCopy(saltBytes, 0, preHashed, passwordBytes.Length, saltBytes.Length); 

    SHA1 sha1 = SHA1.Create(); 
    return sha1.ComputeHash(preHashed); 
} 

Kiểm tra mật khẩu có thể được thực hiện đơn giản bằng cách tính toán băm và sau đó so sánh nó với băm dự kiến.


private bool IsPasswordValid(string passwordToValidate, int salt, byte[] correctPasswordHash) 
{ 
    byte[] hashedPassword = ComputePasswordHash(passwordToValidate, salt); 

    return hashedPassword.SequenceEqual(correctPasswordHash); 
} 

+4

-1 cho đồng bằng SHA-1. Sử dụng một hàm băm chậm, chẳng hạn như PBKDF2, bcrypt hoặc scrypt. Nó cũng được khuyến khích sử dụng một muối 64 bit, nhưng đó là một vấn đề nhỏ. – CodesInChaos

2

PBKDF2 đang sử dụng HMACSHA1 .......nếu bạn muốn HMACSHA256 hoặc HMACSHA512 thực hiện hiện đại hơn và vẫn muốn có chìa khóa kéo dài để làm cho thuật toán chậm Tôi đề nghị API này: https://sourceforge.net/projects/pwdtknet/

2

Đây là một thực hiện đầy đủ một sự kiên trì không biết lớp SecuredPassword

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Security.Cryptography; 
using System.Text; 
using System.Threading.Tasks; 


    public class SecuredPassword 
    { 
     private const int saltSize = 256; 
     private readonly byte[] hash; 
     private readonly byte[] salt; 

     public byte[] Hash 
     { 
     get { return hash; } 
    } 

    public byte[] Salt 
    { 
     get { return salt; } 
    } 

    public SecuredPassword(string plainPassword) 
    { 
     if (string.IsNullOrWhiteSpace(plainPassword)) 
      return; 

     using (var deriveBytes = new Rfc2898DeriveBytes(plainPassword, saltSize)) 
     { 
      salt = deriveBytes.Salt; 
      hash = deriveBytes.GetBytes(saltSize); 
     } 
    } 

    public SecuredPassword(byte[] hash, byte[] salt) 
    { 
     this.hash = hash; 
     this.salt = salt; 
    } 

    public bool Verify(string password) 
    { 
     if (string.IsNullOrWhiteSpace(password)) 
      return false; 

     using (var deriveBytes = new Rfc2898DeriveBytes(password, salt)) 
     { 
      byte[] newKey = deriveBytes.GetBytes(saltSize); 

      return newKey.SequenceEqual(hash); 
     } 
    } 
} 

Và kiểm tra:

public class SecuredPasswordTests 
{ 
    [Test] 
    public void IsHashed_AsExpected() 
    { 
     var securedPassword = new SecuredPassword("password"); 

     Assert.That(securedPassword.Hash, Is.Not.EqualTo("password")); 
     Assert.That(securedPassword.Hash.Length, Is.EqualTo(256)); 
    } 

    [Test] 
    public void Generates_Unique_Salt() 
    { 
     var securedPassword = new SecuredPassword("password"); 
     var securedPassword2 = new SecuredPassword("password"); 

     Assert.That(securedPassword.Salt, Is.Not.Null); 
     Assert.That(securedPassword2.Salt, Is.Not.Null); 
     Assert.That(securedPassword.Salt, Is.Not.EqualTo(securedPassword2.Salt)); 
    } 

    [Test] 
    public void Generates_Unique_Hash() 
    { 
     var securedPassword = new SecuredPassword("password"); 
     var securedPassword2 = new SecuredPassword("password"); 

     Assert.That(securedPassword.Hash, Is.Not.Null); 
     Assert.That(securedPassword2.Hash, Is.Not.Null); 
     Assert.That(securedPassword.Hash, Is.Not.EqualTo(securedPassword2.Hash)); 
    } 

    [Test] 
    public void Verify_WhenMatching_ReturnsTrue() 
    { 
     var securedPassword = new SecuredPassword("password"); 
     var result = securedPassword.Verify("password"); 
     Assert.That(result, Is.True); 
    } 

    [Test] 
    public void Verify_WhenDifferent_ReturnsFalse() 
    { 
     var securedPassword = new SecuredPassword("password"); 
     var result = securedPassword.Verify("Password"); 
     Assert.That(result, Is.False); 
    } 

    [Test] 
    public void Verify_WhenRehydrated_AndMatching_ReturnsTrue() 
    { 
     var securedPassword = new SecuredPassword("password123"); 

     var rehydrated = new SecuredPassword(securedPassword.Hash, securedPassword.Salt); 

     var result = rehydrated.Verify("password123"); 
     Assert.That(result, Is.True); 
    } 

    [Test] 
    public void Constructor_Handles_Null_Password() 
    { 
     Assert.DoesNotThrow(() => new SecuredPassword(null)); 
    } 

    [Test] 
    public void Constructor_Handles_Empty_Password() 
    { 
     Assert.DoesNotThrow(() => new SecuredPassword(string.Empty)); 
    } 

    [Test] 
    public void Verify_Handles_Null_Password() 
    { 
     Assert.DoesNotThrow(() => new SecuredPassword("password").Verify(null)); 
    } 

    [Test] 
    public void Verify_Handles_Empty_Password() 
    { 
     Assert.DoesNotThrow(() => new SecuredPassword("password").Verify(string.Empty)); 
    } 

    [Test] 
    public void Verify_When_Null_Password_ReturnsFalse() 
    { 
     Assert.That(new SecuredPassword("password").Verify(null), Is.False); 
    } 
} 
1

Vui lòng sử dụng điều này như tôi có vấn đề tương tự trước đây, nhưng có thể giải quyết nó sẽ mã litle đoạn mã

public static string ComputeHash(string input, HashAlgorithm algorithm, Byte[] salt) 
    { 
     Byte[] inputBytes = Encoding.UTF8.GetBytes(input); 

     // Combine salt and input bytes 
     Byte[] saltedInput = new Byte[salt.Length + inputBytes.Length]; 
     salt.CopyTo(saltedInput, 0); 
     inputBytes.CopyTo(saltedInput, salt.Length); 

     Byte[] hashedBytes = algorithm.ComputeHash(saltedInput); 


     StringBuilder hex = new StringBuilder(hashedBytes.Length * 2); 
     foreach (byte b in hashedBytes) 
      hex.AppendFormat("{0:X2}", b); 

     return hex.ToString(); 

    } 
+0

Bạn nên thêm giải thích và xem [Làm cách nào để đặt câu hỏi hay?] (Https://stackoverflow.com/help/how-to-ask) – CodingNinja

0

TL; DR sử dụng Microsoft.AspNetCore.Cryptography.KeyDerivation, triển khai PBKDF2 với SHA-512.

Ý tưởng hay để bắt đầu với băm mật khẩu là xem xét những gì OWASP guidelines nói. Danh sách các thuật toán được đề xuất bao gồm Argon2, PBKDF2, scrypt và bcrypt. Tất cả các thuật toán này có thể được điều chỉnh để điều chỉnh thời gian cần thiết để băm mật khẩu, và tương ứng, thời gian để crack nó thông qua brute-force. Tất cả các thuật toán này sử dụng muối để bảo vệ khỏi các cuộc tấn công của bảng cầu vồng.

Cả của các thuật toán là terribly yếu, nhưng có một số khác biệt:

  • bcrypt đã được khoảng gần 20 năm, đã được sử dụng rộng rãi và đã chịu đựng sự thử thách của thời gian. Nó là khá khả năng chống lại các cuộc tấn công GPU , nhưng không phải để FPGA
  • Argon2 là bổ sung mới nhất, là người chiến thắng của cuộc thi băm mật khẩu năm 2015. Nó có bảo vệ tốt hơn chống lại các cuộc tấn công GPU và FPGA, nhưng hơi quá gần với ý thích của tôi
  • Tôi không biết nhiều về việc giải mã. Nó đã được thiết kế để ngăn chặn các cuộc tấn công tăng tốc GPU và FPGA, nhưng tôi đã nghe nói nó không mạnh như đã tuyên bố ban đầu
  • PBKDF2 là họ các thuật toán được tham số bởi hàm băm khác nhau . Nó không cung cấp một sự bảo vệ cụ thể chống lại các cuộc tấn công của GPU hoặc ASIC, đặc biệt nếu một hàm băm yếu hơn như SHA-1 được sử dụng, nhưng nó được chứng nhận FIPS nếu nó quan trọng với bạn, và vẫn chấp nhận được nếu số lần lặp lại là đủ lớn.

Chỉ dựa trên thuật toán, tôi có thể đi với bcrypt, PBKDF2 là ít thuận lợi nhất.

Tuy nhiên, nó không phải là toàn bộ câu chuyện, bởi vì ngay cả các thuật toán tốt nhất có thể được thực hiện không an toàn bởi một thực hiện xấu. Hãy xem những gì có sẵn cho nền tảng .NET:

  • Bcrypt có sẵn qua bcrypt.net. Họ nói rằng việc thực hiện dựa trên Java jBCrypt. Hiện tại có 6 người đóng góp và 8 vấn đề (tất cả đã đóng) trên github. Nhìn chung, nó có vẻ tốt, tuy nhiên, tôi không biết nếu có ai đã thực hiện một kiểm toán của mã, và thật khó để nói liệu một phiên bản cập nhật sẽ có sẵn đủ sớm nếu một lỗ hổng được tìm thấy. Tôi đã nghe Stack Overflow chuyển từ sử dụng bcrypt vì những lý do như vậy
  • Có lẽ cách tốt nhất để sử dụng Argon2 là thông qua các ràng buộc với thư viện libsodium nổi tiếng , ví dụ: https://github.com/adamcaudill/libsodium-net. Ý tưởng là hầu hết các mật mã được thực hiện thông qua libsodium, trong đó có đáng kể hỗ trợ, và các phần 'chưa được kiểm tra' là khá hạn chế.Tuy nhiên, trong chi tiết mật mã có nghĩa là rất nhiều, vì vậy kết hợp với Argon2 là tương đối gần đây, tôi muốn đối xử với nó như một tùy chọn thực nghiệm
  • Trong một thời gian dài, NET có một built-in một thực hiện PBKDF2 qua Rfc2898DeriveBytes lớp học. Tuy nhiên, việc triển khai chỉ có thể sử dụng hàm băm SHA-1, được coi là quá nhanh để bảo mật hiện nay
  • Cuối cùng, giải pháp gần đây nhất là Microsoft.AspNetCore.Cryptography.KeyDerivation gói có sẵn qua NuGet. Nó cung cấp thuật toán PBKDF2 với hàm băm SHA-1, SHA-256 hoặc SHA-512, tốt hơn đáng kể so với Rfc2898DeriveBytes. Lợi thế lớn nhất ở đây là việc triển khai được cung cấp bởi Microsoft, và trong khi tôi không thể đánh giá đúng đắn sự tinh tấn về mật mã của các nhà phát triển Microsoft so với các nhà phát triển BCrypt.net hoặc libsodium, nó chỉ có ý nghĩa để tin tưởng nó bởi vì nếu bạn đang chạy một ứng dụng .NET, bạn đang phụ thuộc rất nhiều vào Microsoft rồi. Chúng tôi cũng có thể mong đợi Microsoft phát hành bản cập nhật nếu tìm thấy các vấn đề bảo mật. Hy vọng.

Để tóm tắt nghiên cứu cho đến thời điểm này, trong khi PBKDF2 có thể là thuật toán ít được ưu tiên nhất, khả năng cung cấp do Microsoft cung cấp vượt trội hơn, vì vậy quyết định hợp lý sẽ sử dụng Microsoft.AspNetCore.Cryptography.KeyDerivation.

Gói gần đây tại thời điểm này nhắm mục tiêu .NET Standard 2.0, do đó có sẵn trong .NET Core 2.0 hoặc .NET Framework 4.6.1 trở lên. Nếu bạn sử dụng phiên bản khung trước đó, bạn có thể sử dụng phiên bản trước của gói, 1.1.3, nhắm mục tiêu Khuôn khổ .NET 4.5.1 hoặc .NET Core 1.0. Thật không may, nó không thể sử dụng nó trong các phiên bản .NET cũ hơn.

Tài liệu và ví dụ làm việc có sẵn tại docs.microsoft.com. Tuy nhiên, không sao chép-dán nó như nó được, vẫn còn có quyết định một nhà phát triển cần phải thực hiện.

Quyết định đầu tiên là hàm băm sử dụng. Các tùy chọn có sẵn bao gồm SHA-1, SHA-256 và SHA-512. Trong số đó, SHA-1 chắc chắn là quá nhanh để bảo mật, SHA-256 là tốt, nhưng tôi khuyên bạn nên sử dụng SHA-512, vì được cho là, hoạt động 64 bit của nó khiến cho việc tấn công dựa trên GPU trở nên khó khăn hơn.

Sau đó, bạn cần chọn độ dài đầu ra băm mật khẩu và độ dài muối. Nó không có ý nghĩa để có đầu ra dài hơn đầu ra hàm băm (ví dụ: 512 bit cho SHA-512), và nó có lẽ là an toàn nhất để có nó chính xác như thế. Đối với chiều dài muối, ý kiến ​​khác nhau. 128 bit là đủ, nhưng trong mọi trường hợp, độ dài dài hơn độ dài đầu ra băm chắc chắn không cung cấp bất kỳ lợi ích nào.

Tiếp theo, có số lần lặp lại. Nó càng lớn thì các băm mật khẩu càng khó crack, nhưng mất nhiều thời gian hơn để đăng nhập người dùng. Tôi đề nghị chọn nó để băm băm mất 0,25 - 1 giây trên hệ thống sản xuất điển hình, và trong mọi trường hợp, nó không được nhỏ hơn 10000.

Thông thường, bạn sẽ nhận được mảng byte dưới dạng giá trị muối và giá trị băm. Sử dụng Base64 để chuyển đổi chúng thành chuỗi. Bạn có thể chọn sử dụng hai cột khác nhau trong cơ sở dữ liệu, hoặc kết hợp muối và mật khẩu trong một cột bằng cách sử dụng dấu tách không gặp phải trong Base64.

Đừng quên tạo bộ nhớ băm mật khẩu theo cách cho phép di chuyển liên tục sang thuật toán băm tốt hơn trong tương lai.

Các vấn đề liên quan