Cara mencirikan kata sandi


117

Saya ingin menyimpan hash kata sandi di telepon, tetapi saya tidak yakin bagaimana melakukannya. Saya hanya dapat menemukan metode enkripsi. Bagaimana seharusnya kata sandi di-hash dengan benar?

Jawaban:


62

PEMBARUAN : JAWABAN INI SANGAT KELUAR . Silakan gunakan rekomendasi dari https://stackoverflow.com/a/10402129/251311 sebagai gantinya.

Anda bisa menggunakan

var md5 = new MD5CryptoServiceProvider();
var md5data = md5.ComputeHash(data);

atau

var sha1 = new SHA1CryptoServiceProvider();
var sha1data = sha1.ComputeHash(data);

Untuk mendapatkan dataarray byte bisa Anda gunakan

var data = Encoding.ASCII.GetBytes(password);

dan untuk mendapatkan kembali string dari md5dataatausha1data

var hashedPassword = ASCIIEncoding.GetString(md5data);

11
Saya BENAR-BENAR akan merekomendasikan menggunakan SHA1. MD5 adalah tidak-tidak kecuali Anda menjaga kompatibilitas dengan sistem yang ada. Selain itu, pastikan Anda memasukkannya ke dalam usingpernyataan atau memanggilnya Clear()setelah selesai menggunakan implementasi.
vcsjones

3
@vcsjones: Saya tidak ingin perang suci di sini, tetapi md5cukup baik untuk hampir semua jenis tugas. Kerentanannya juga mengacu pada situasi yang sangat spesifik dan hampir mengharuskan penyerang untuk mengetahui banyak tentang kriptografi.
zerkms

4
@zerkms titik diambil, tetapi jika tidak ada alasan untuk kompatibilitas mundur, tidak ada alasan untuk menggunakan MD5. "Lebih baik aman daripada menyesal".
vcsjones

4
Tidak ada alasan untuk menggunakan MD5 saat ini. Mengingat waktu komputasi yang tidak signifikan, tidak ada alasan untuk menggunakan MD5 kecuali sebagai kompatibilitas dengan sistem yang ada. Meskipun MD5 "cukup baik", tidak ada biaya bagi pengguna SHA yang jauh lebih aman. Saya yakin zerkms tahu ini komentarnya lebih untuk si penanya.
Gerald Davis

11
Tiga kesalahan besar: 1) ASCII secara diam-diam menurunkan sandi dengan karakter yang tidak biasa 2) MD5 / SHA-1 / SHA-2 biasa cepat. 3) Anda membutuhkan garam. | Gunakan PBKDF2, bcrypt atau scrypt sebagai gantinya. PBKDF2 paling mudah di kelas Rfc2898DeriveBytes (tidak yakin apakah ada di WP7)
CodesInChaos

298

Sebagian besar jawaban lain di sini agak ketinggalan zaman dengan praktik terbaik saat ini. Seperti inilah aplikasi menggunakan PBKDF2 / Rfc2898DeriveBytesuntuk menyimpan dan memverifikasi password. Kode berikut berada di kelas yang berdiri sendiri di posting ini: Contoh lain tentang cara menyimpan hash kata sandi asin . Dasar-dasarnya sangat mudah, jadi inilah uraiannya:

LANGKAH 1 Buat nilai garam dengan PRNG kriptografi:

byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);

LANGKAH 2 Buat Rfc2898DeriveBytes dan dapatkan nilai hash:

var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);

LANGKAH 3 Gabungkan byte garam dan sandi untuk digunakan nanti:

byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);

LANGKAH 4 Ubah gabungan garam + hash menjadi string untuk disimpan

string savedPasswordHash = Convert.ToBase64String(hashBytes);
DBContext.AddUser(new User { ..., Password = savedPasswordHash });

LANGKAH 5 Verifikasi kata sandi yang dimasukkan pengguna dengan kata sandi yang disimpan

/* Fetch the stored value */
string savedPasswordHash = DBContext.GetUser(u => u.UserName == user).Password;
/* Extract the bytes */
byte[] hashBytes = Convert.FromBase64String(savedPasswordHash);
/* Get the salt */
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
/* Compute the hash on the password the user entered */
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
/* Compare the results */
for (int i=0; i < 20; i++)
    if (hashBytes[i+16] != hash[i])
        throw new UnauthorizedAccessException();

Catatan: Bergantung pada persyaratan kinerja aplikasi spesifik Anda, nilainya 100000dapat dikurangi. Nilai minimum harus sekitar 10000.


8
@Daniel pada dasarnya posting adalah tentang menggunakan sesuatu yang lebih aman daripada hash saja. Jika Anda hanya mencirikan kata sandi, bahkan dengan garam, kata sandi pengguna Anda akan disusupi (dan kemungkinan dijual / diterbitkan) bahkan sebelum Anda memiliki kesempatan untuk memberi tahu mereka untuk mengubahnya. Gunakan kode di atas untuk mempersulit penyerang, tidak mudah bagi pengembang.
csharptest.net

2
@DatVM Tidak, garam baru untuk setiap kali Anda menyimpan hash. itulah mengapa itu digabungkan dengan hash untuk penyimpanan sehingga Anda dapat memverifikasi kata sandi.
csharptest.net

9
@CiprianJijie intinya adalah Anda tidak seharusnya bisa.
csharptest.net

9
Jika ada yang melakukan metode VerifyPassword, jika Anda ingin menggunakan Linq dan panggilan yang lebih pendek untuk boolean, ini akan dilakukan: return hash.SequenceEqual (hashBytes.Skip (_saltSize));
Jesú Castillo

2
@ csharptest.net Apa jenis ukuran larik yang Anda rekomendasikan? apakah ukuran array mempengaruhi banyak keamanan? Saya tidak tahu banyak tentang hashing / kriptografi
lennyy

71

Berdasarkan jawaban bagus csharptest.net , saya telah menulis Kelas untuk ini:

public static class SecurePasswordHasher
{
    /// <summary>
    /// Size of salt.
    /// </summary>
    private const int SaltSize = 16;

    /// <summary>
    /// Size of hash.
    /// </summary>
    private const int HashSize = 20;

    /// <summary>
    /// Creates a hash from a password.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="iterations">Number of iterations.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password, int iterations)
    {
        // Create salt
        byte[] salt;
        new RNGCryptoServiceProvider().GetBytes(salt = new byte[SaltSize]);

        // Create hash
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
        var hash = pbkdf2.GetBytes(HashSize);

        // Combine salt and hash
        var hashBytes = new byte[SaltSize + HashSize];
        Array.Copy(salt, 0, hashBytes, 0, SaltSize);
        Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);

        // Convert to base64
        var base64Hash = Convert.ToBase64String(hashBytes);

        // Format hash with extra information
        return string.Format("$MYHASH$V1${0}${1}", iterations, base64Hash);
    }

    /// <summary>
    /// Creates a hash from a password with 10000 iterations
    /// </summary>
    /// <param name="password">The password.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password)
    {
        return Hash(password, 10000);
    }

    /// <summary>
    /// Checks if hash is supported.
    /// </summary>
    /// <param name="hashString">The hash.</param>
    /// <returns>Is supported?</returns>
    public static bool IsHashSupported(string hashString)
    {
        return hashString.Contains("$MYHASH$V1$");
    }

    /// <summary>
    /// Verifies a password against a hash.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="hashedPassword">The hash.</param>
    /// <returns>Could be verified?</returns>
    public static bool Verify(string password, string hashedPassword)
    {
        // Check hash
        if (!IsHashSupported(hashedPassword))
        {
            throw new NotSupportedException("The hashtype is not supported");
        }

        // Extract iteration and Base64 string
        var splittedHashString = hashedPassword.Replace("$MYHASH$V1$", "").Split('$');
        var iterations = int.Parse(splittedHashString[0]);
        var base64Hash = splittedHashString[1];

        // Get hash bytes
        var hashBytes = Convert.FromBase64String(base64Hash);

        // Get salt
        var salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);

        // Create hash with given salt
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
        byte[] hash = pbkdf2.GetBytes(HashSize);

        // Get result
        for (var i = 0; i < HashSize; i++)
        {
            if (hashBytes[i + SaltSize] != hash[i])
            {
                return false;
            }
        }
        return true;
    }
}

Pemakaian:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

Contoh hash bisa jadi ini:

$MYHASH$V1$10000$Qhxzi6GNu/Lpy3iUqkeqR/J1hh8y/h5KPDjrv89KzfCVrubn

Seperti yang Anda lihat, saya juga telah memasukkan iterasi dalam hash untuk penggunaan yang mudah dan kemungkinan untuk meningkatkan ini, jika kita perlu meningkatkan.


Jika Anda tertarik dengan .net core, saya juga memiliki versi inti .net di Code Review .


1
Hanya untuk memverifikasi, jika Anda meningkatkan mesin hashing, Anda akan menaikkan bagian V1 dari hash dan kunci Anda?
Mike Cole

1
Ya itu rencananya. Anda kemudian akan memutuskan berdasarkan V1dan V2metode verifikasi mana yang Anda butuhkan.
Christian Gollhardt

Terima kasih atas jawabannya, dan kelasnya. Saya menerapkannya saat kita berbicara.
Mike Cole

2
Ya @Nonon. Itu karena garam .
Christian Gollhardt

1
Dengan semua salinan / tempel kode ini (termasuk saya), saya harap seseorang angkat bicara dan kiriman direvisi jika ditemukan masalah dengannya! :)
pettys

14

Saya menggunakan hash dan salt untuk enkripsi kata sandi saya (hash yang sama dengan yang digunakan oleh Asp.Net Membership):

private string PasswordSalt
{
   get
   {
      var rng = new RNGCryptoServiceProvider();
      var buff = new byte[32];
      rng.GetBytes(buff);
      return Convert.ToBase64String(buff);
   }
}

private string EncodePassword(string password, string salt)
{
   byte[] bytes = Encoding.Unicode.GetBytes(password);
   byte[] src = Encoding.Unicode.GetBytes(salt);
   byte[] dst = new byte[src.Length + bytes.Length];
   Buffer.BlockCopy(src, 0, dst, 0, src.Length);
   Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
   HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
   byte[] inarray = algorithm.ComputeHash(dst);
   return Convert.ToBase64String(inarray);
}

16
-1 untuk menggunakan SHA-1 biasa, yang cepat. Gunakan fungsi derivasi kunci lambat, seperti PBKDF2, bcrypt atau scrypt.
CodesInChaos

1
  1. Buat garam,
  2. Buat kata sandi hash dengan salt
  3. Simpan hash dan garam
  4. mendekripsi dengan kata sandi dan garam ... sehingga pengembang tidak dapat mendekripsi kata sandi
public class CryptographyProcessor
{
    public string CreateSalt(int size)
    {
        //Generate a cryptographic random number.
          RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
         byte[] buff = new byte[size];
         rng.GetBytes(buff);
         return Convert.ToBase64String(buff);
    }


      public string GenerateHash(string input, string salt)
      { 
         byte[] bytes = Encoding.UTF8.GetBytes(input + salt);
         SHA256Managed sHA256ManagedString = new SHA256Managed();
         byte[] hash = sHA256ManagedString.ComputeHash(bytes);
         return Convert.ToBase64String(hash);
      }

      public bool AreEqual(string plainTextInput, string hashedInput, string salt)
      {
           string newHashedPin = GenerateHash(plainTextInput, salt);
           return newHashedPin.Equals(hashedInput); 
      }
 }

1

Jawaban @ csharptest.net dan Christian Gollhardt sangat bagus, terima kasih banyak. Tetapi setelah menjalankan kode ini pada produksi dengan jutaan catatan, saya menemukan ada kebocoran memori. Kelas RNGCryptoServiceProvider dan Rfc2898DeriveBytes diturunkan dari IDisposable tetapi kami tidak membuangnya. Saya akan menulis solusi saya sebagai jawaban jika seseorang membutuhkan dengan versi yang dibuang.

public static class SecurePasswordHasher
{
    /// <summary>
    /// Size of salt.
    /// </summary>
    private const int SaltSize = 16;

    /// <summary>
    /// Size of hash.
    /// </summary>
    private const int HashSize = 20;

    /// <summary>
    /// Creates a hash from a password.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="iterations">Number of iterations.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password, int iterations)
    {
        // Create salt
        using (var rng = new RNGCryptoServiceProvider())
        {
            byte[] salt;
            rng.GetBytes(salt = new byte[SaltSize]);
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
            {
                var hash = pbkdf2.GetBytes(HashSize);
                // Combine salt and hash
                var hashBytes = new byte[SaltSize + HashSize];
                Array.Copy(salt, 0, hashBytes, 0, SaltSize);
                Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
                // Convert to base64
                var base64Hash = Convert.ToBase64String(hashBytes);

                // Format hash with extra information
                return $"$HASH|V1${iterations}${base64Hash}";
            }
        }

    }

    /// <summary>
    /// Creates a hash from a password with 10000 iterations
    /// </summary>
    /// <param name="password">The password.</param>
    /// <returns>The hash.</returns>
    public static string Hash(string password)
    {
        return Hash(password, 10000);
    }

    /// <summary>
    /// Checks if hash is supported.
    /// </summary>
    /// <param name="hashString">The hash.</param>
    /// <returns>Is supported?</returns>
    public static bool IsHashSupported(string hashString)
    {
        return hashString.Contains("HASH|V1$");
    }

    /// <summary>
    /// Verifies a password against a hash.
    /// </summary>
    /// <param name="password">The password.</param>
    /// <param name="hashedPassword">The hash.</param>
    /// <returns>Could be verified?</returns>
    public static bool Verify(string password, string hashedPassword)
    {
        // Check hash
        if (!IsHashSupported(hashedPassword))
        {
            throw new NotSupportedException("The hashtype is not supported");
        }

        // Extract iteration and Base64 string
        var splittedHashString = hashedPassword.Replace("$HASH|V1$", "").Split('$');
        var iterations = int.Parse(splittedHashString[0]);
        var base64Hash = splittedHashString[1];

        // Get hash bytes
        var hashBytes = Convert.FromBase64String(base64Hash);

        // Get salt
        var salt = new byte[SaltSize];
        Array.Copy(hashBytes, 0, salt, 0, SaltSize);

        // Create hash with given salt
        using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
        {
            byte[] hash = pbkdf2.GetBytes(HashSize);

            // Get result
            for (var i = 0; i < HashSize; i++)
            {
                if (hashBytes[i + SaltSize] != hash[i])
                {
                    return false;
                }
            }

            return true;
        }

    }
}

Pemakaian:

// Hash
var hash = SecurePasswordHasher.Hash("mypassword");

// Verify
var result = SecurePasswordHasher.Verify("mypassword", hash);

0

Saya pikir menggunakan KeyDerivation.Pbkdf2 lebih baik daripada Rfc2898DeriveBytes.

Contoh dan penjelasan: Kata sandi hash di ASP.NET Core

using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
 
public class Program
{
    public static void Main(string[] args)
    {
        Console.Write("Enter a password: ");
        string password = Console.ReadLine();
 
        // generate a 128-bit salt using a secure PRNG
        byte[] salt = new byte[128 / 8];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(salt);
        }
        Console.WriteLine($"Salt: {Convert.ToBase64String(salt)}");
 
        // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations)
        string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
            password: password,
            salt: salt,
            prf: KeyDerivationPrf.HMACSHA1,
            iterationCount: 10000,
            numBytesRequested: 256 / 8));
        Console.WriteLine($"Hashed: {hashed}");
    }
}
 
/*
 * SAMPLE OUTPUT
 *
 * Enter a password: Xtw9NMgx
 * Salt: NZsP6NnmfBuYeJrrAKNuVQ==
 * Hashed: /OOoOer10+tGwTRDTrQSoeCxVTFr6dtYly7d0cPxIak=
 */

Ini adalah contoh kode dari artikel. Dan itu adalah tingkat keamanan minimum. Untuk meningkatkannya, saya akan menggunakan parameter KeyDerivationPrf.HMACSHA1

KeyDerivationPrf.HMACSHA256 atau KeyDerivationPrf.HMACSHA512.

Jangan berkompromi dengan hashing kata sandi. Ada banyak metode yang terdengar secara matematis untuk mengoptimalkan peretasan hash kata sandi. Konsekuensinya bisa menjadi bencana. Setelah penjahat bisa mendapatkan tabel hash kata sandi pengguna Anda, akan relatif mudah baginya untuk memecahkan kata sandi karena algoritme lemah atau penerapannya salah. Dia punya banyak waktu (waktu x kekuatan komputer) untuk memecahkan kata sandi. Pencirian sandi harus kuat secara kriptografis untuk mengubah "banyak waktu" menjadi "waktu yang tidak wajar ".

Satu hal lagi untuk ditambahkan

Verifikasi hash membutuhkan waktu (dan itu bagus). Ketika pengguna memasukkan nama pengguna yang salah, tidak perlu waktu lama untuk memeriksa bahwa nama pengguna itu salah. Ketika nama pengguna benar, kami memulai verifikasi kata sandi - ini proses yang relatif lama.

Bagi seorang hacker akan sangat mudah untuk memahami apakah pengguna itu ada atau tidak.

Pastikan untuk tidak langsung memberikan jawaban jika nama pengguna salah.

Tak perlu dikatakan: jangan pernah memberikan jawaban apa yang salah. Hanya umum "Kredensial salah".


1
BTW, jawaban stackoverflow.com/a/57508528/11603057 sebelumnya tidak benar dan berbahaya. Itu adalah contoh hashing, bukan hashing kata sandi. Harus merupakan iterasi dari fungsi pseudo-random selama proses penurunan kunci. Tidak ada. Saya tidak bisa berkomentar atau memberi suara negatif (reputasi saya yang rendah). Tolong jangan lewatkan jawaban yang tidak benar!
Albert Lyubarsky
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.