I was just going through one of DavidHayden's articles on Hashing User Passwords.
Really I can't get what he is trying to achieve.
Here is his code:
private static string CreateSalt(int size)
{
//Generate a cryptographic random number.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] buff = new byte[size];
rng.GetBytes(buff);
// Return a Base64 string representation of the random number.
return Convert.ToBase64String(buff);
}
private static string CreatePasswordHash(string pwd, string salt)
{
string saltAndPwd = String.Concat(pwd, salt);
string hashedPwd =
FormsAuthentication.HashPasswordForStoringInConfigFile(
saltAndPwd, "sha1");
return ha开发者_如何学JAVAshedPwd;
}
Is there any other C# method for hashing passwords and adding salt to it?
Actually this is kind of strange, with the string conversions - which the membership provider does to put them into config files. Hashes and salts are binary blobs, you don't need to convert them to strings unless you want to put them into text files.
In my book, Beginning ASP.NET Security, (oh finally, an excuse to pimp the book) I do the following
static byte[] GenerateSaltedHash(byte[] plainText, byte[] salt)
{
HashAlgorithm algorithm = new SHA256Managed();
byte[] plainTextWithSaltBytes =
new byte[plainText.Length + salt.Length];
for (int i = 0; i < plainText.Length; i++)
{
plainTextWithSaltBytes[i] = plainText[i];
}
for (int i = 0; i < salt.Length; i++)
{
plainTextWithSaltBytes[plainText.Length + i] = salt[i];
}
return algorithm.ComputeHash(plainTextWithSaltBytes);
}
The salt generation is as the example in the question. You can convert text to byte arrays using Encoding.UTF8.GetBytes(string)
. If you must convert a hash to its string representation you can use Convert.ToBase64String
and Convert.FromBase64String
to convert it back.
You should note that you cannot use the equality operator on byte arrays, it checks references and so you should simply loop through both arrays checking each byte thus
public static bool CompareByteArrays(byte[] array1, byte[] array2)
{
if (array1.Length != array2.Length)
{
return false;
}
for (int i = 0; i < array1.Length; i++)
{
if (array1[i] != array2[i])
{
return false;
}
}
return true;
}
Always use a new salt per password. Salts do not have to be kept secret and can be stored alongside the hash itself.
What blowdart said, but with a little less code. Use Linq or CopyTo
to concatenate arrays.
public static byte[] Hash(string value, byte[] salt)
{
return Hash(Encoding.UTF8.GetBytes(value), salt);
}
public static byte[] Hash(byte[] value, byte[] salt)
{
byte[] saltedValue = value.Concat(salt).ToArray();
// Alternatively use CopyTo.
//var saltedValue = new byte[value.Length + salt.Length];
//value.CopyTo(saltedValue, 0);
//salt.CopyTo(saltedValue, value.Length);
return new SHA256Managed().ComputeHash(saltedValue);
}
Linq has an easy way to compare your byte arrays too.
public bool ConfirmPassword(string password)
{
byte[] passwordHash = Hash(password, _passwordSalt);
return _passwordHash.SequenceEqual(passwordHash);
}
Before implementing any of this however, check out this post. For password hashing you may want a slow hash algorithm, not a fast one.
To that end there is the Rfc2898DeriveBytes
class which is slow (and can be made slower), and may answer the second part of the original question in that it can take a password and salt and return a hash. See this question for more information. Note, Stack Exchange is using Rfc2898DeriveBytes
for password hashing (source code here).
I've been reading that hashing functions like SHA256 weren't really intended for use with storing passwords: https://patrickmn.com/security/storing-passwords-securely/#notpasswordhashes
Instead adaptive key derivation functions like PBKDF2, bcrypt or scrypt were. Here is a PBKDF2 based one that Microsoft wrote for PasswordHasher in their Microsoft.AspNet.Identity library:
/* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 3:
* PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
public string HashPassword(string password)
{
var prf = KeyDerivationPrf.HMACSHA256;
var rng = RandomNumberGenerator.Create();
const int iterCount = 10000;
const int saltSize = 128 / 8;
const int numBytesRequested = 256 / 8;
// Produce a version 3 (see comment above) text hash.
var salt = new byte[saltSize];
rng.GetBytes(salt);
var subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
var outputBytes = new byte[13 + salt.Length + subkey.Length];
outputBytes[0] = 0x01; // format marker
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
WriteNetworkByteOrder(outputBytes, 5, iterCount);
WriteNetworkByteOrder(outputBytes, 9, saltSize);
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
return Convert.ToBase64String(outputBytes);
}
public bool VerifyHashedPassword(string hashedPassword, string providedPassword)
{
var decodedHashedPassword = Convert.FromBase64String(hashedPassword);
// Wrong version
if (decodedHashedPassword[0] != 0x01)
return false;
// Read header information
var prf = (KeyDerivationPrf)ReadNetworkByteOrder(decodedHashedPassword, 1);
var iterCount = (int)ReadNetworkByteOrder(decodedHashedPassword, 5);
var saltLength = (int)ReadNetworkByteOrder(decodedHashedPassword, 9);
// Read the salt: must be >= 128 bits
if (saltLength < 128 / 8)
{
return false;
}
var salt = new byte[saltLength];
Buffer.BlockCopy(decodedHashedPassword, 13, salt, 0, salt.Length);
// Read the subkey (the rest of the payload): must be >= 128 bits
var subkeyLength = decodedHashedPassword.Length - 13 - salt.Length;
if (subkeyLength < 128 / 8)
{
return false;
}
var expectedSubkey = new byte[subkeyLength];
Buffer.BlockCopy(decodedHashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
// Hash the incoming password and verify it
var actualSubkey = KeyDerivation.Pbkdf2(providedPassword, salt, prf, iterCount, subkeyLength);
return actualSubkey.SequenceEqual(expectedSubkey);
}
private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
buffer[offset + 0] = (byte)(value >> 24);
buffer[offset + 1] = (byte)(value >> 16);
buffer[offset + 2] = (byte)(value >> 8);
buffer[offset + 3] = (byte)(value >> 0);
}
private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{
return ((uint)(buffer[offset + 0]) << 24)
| ((uint)(buffer[offset + 1]) << 16)
| ((uint)(buffer[offset + 2]) << 8)
| ((uint)(buffer[offset + 3]));
}
Note this requires Microsoft.AspNetCore.Cryptography.KeyDerivation nuget package installed which requires .NET Standard 2.0 (.NET 4.6.1 or higher). For earlier versions of .NET see the Crypto class from Microsoft's System.Web.Helpers library.
Update Nov 2015
Updated answer to use an implementation from a different Microsoft library which uses PBKDF2-HMAC-SHA256 hashing instead of PBKDF2-HMAC-SHA1 (note PBKDF2-HMAC-SHA1 is still secure if iterCount is high enough). You can check out the source the simplified code was copied from as it actually handles validating and upgrading hashes implemented from previous answer, useful if you need to increase iterCount in the future.
Salt is used to add an extra level of complexity to the hash, to make it harder to brute-force crack.
From an article on Sitepoint:
A hacker can still perform what's called a dictionary attack. Malicious parties may make a dictionary attack by taking, for instance, 100,000 passwords that they know people use frequently (e.g. city names, sports teams, etc.), hash them, and then compare each entry in the dictionary against each row in the database table. If the hackers find a match, bingo! They have your password. To solve this problem, however, we need only salt the hash.
To salt a hash, we simply come up with a random-looking string of text, concatenate it with the password supplied by the user, then hash both the randomly generated string and password together as one value. We then save both the hash and the salt as separate fields within the Users table.
In this scenario, not only would a hacker need to guess the password, they'd have to guess the salt as well. Adding salt to the clear text improves security: now, if a hacker tries a dictionary attack, he must hash his 100,000 entries with the salt of every user row. Although it's still possible, the chances of hacking success diminish radically.
There is no method automatically doing this in .NET, so you'll have go with the solution above.
I created a class that has the following method:
Create Salt
Hash Input
Validate input
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); } }
Use the System.Web.Helpers.Crypto
NuGet package from Microsoft. It automatically adds salt to the hash.
You hash a password like this: var hash = Crypto.HashPassword("foo");
You verify a password like this: var verified = Crypto.VerifyHashedPassword(hash, "foo");
I have made a library SimpleHashing.Net to make the process of hashing easy with basic classes provided by Microsoft. Ordinary SHA is not really enough to have passwords stored securely anymore.
The library use the idea of hash format from Bcrypt, but since there is no official MS implementation I prefer to use what's available in the framework (i.e. PBKDF2), but it's a bit too hard out of the box.
This is a quick example how to use the library:
ISimpleHash simpleHash = new SimpleHash();
// Creating a user hash, hashedPassword can be stored in a database
// hashedPassword contains the number of iterations and salt inside it similar to bcrypt format
string hashedPassword = simpleHash.Compute("Password123");
// Validating user's password by first loading it from database by username
string storedHash = _repository.GetUserPasswordHash(username);
isPasswordValid = simpleHash.Verify("Password123", storedHash);
Bah, this is better! http://sourceforge.net/projects/pwdtknet/ and it is better because ..... it performs Key Stretching AND uses HMACSHA512 :)
2022 (.NET 6+) solution:
Most of the other answers here (including the accepted answer) are using the SHA-256 hashing algorithm, which is NOT suited for storing user passwords anymore, even if you use salts. You instead should opt for slower hashing functions for this purpose, such as Bcrypt, Argon2, Scrypt, or PBKDF2; the latter being the only one that's natively available in .NET.
You can find helper methods and whatnot to create PBKDF2 hashes primarily in this other question, but the one I'm providing below has the following advantages over those provided in that question or even some here, such as this one.
Pros:
Uses the new static
Rfc2898DeriveBytes.Pbkdf2()
method introduced in .NET 6, eliminating the need to instantiate and also dispose the object every single time.Uses the new
RandomNumberGenerator
class and its staticGetBytes
method — introduced in .NET 6 — to generate the salt. TheRNGCryptoServiceProvider
class used in the original question and many of the answers here is obsolete.Uses the
CryptographicOperations.FixedTimeEquals
method (introduced in .NET Core 2.1) for comparing the key bytes in theVerify
method, instead of doing the comparison by hand — like the accepted answer is doing. This, in addition to removing a lot of noisy boilerplate, also nullifies timing attacks.Uses SHA-256 instead of the default SHA-1 as the underlying algorithm, just to be on the safe side, as the latter is a more robust and reliable algorithm.
The string that the
Hash
method returns (and by extension the string that theVerify
method receives) has the following structure:[key]:[salt]:[iterations]:[algorithm]
This is the most important advantage of this particular solution; this means that we're basically including metadata about the configurations used to create the hash in the final string. This effectively allows us to change the settings (such as the number of iterations, salt/key size, etc.) in our hasher class in the future, without breaking previous hashes created with the old settings. This is something that most of the other solutions I've come across (that are using PBKDF2) tend to neglect and not actually take into account, although it's crucial. They instead typically rely on the current configuration values to verify hashes, which means that as soon as you decide to change any of the configuration values, any previously-created hashes will no longer be verified properly.
Other points:
- I'm using the hexadecimal representation of the key and the salt in the returned hash string. You can instead use base64 if you prefer, simply by changing every occurrence of
Convert.ToHexString
andConvert.FromHexString
toConvert.ToBase64
andConvert.FromBase64
respectively. The rest of the logic remains exactly the same. - The often recommended salt size is 64 bits or above. I've set it to 128 bits.
- The key size should normally be the same as the natural output size of your chosen algorithm — see this comment. In our case, as I mentioned earlier, the underlying algorithm is SHA-256, whose output size is 256 bits, which is precisely what we're setting our key size to.
- If you plan to use this for storing user passwords, it's usually recommended to use at least 10,000 iterations or more. I've set the default value to 50,000, which you can of course change as you see fit.
The code:
public static class SecretHasher2
{
private const int _saltSize = 16; // 128 bits
private const int _keySize = 32; // 256 bits
private const int _iterations = 100000;
private static readonly HashAlgorithmName _algorithm = HashAlgorithmName.SHA256;
private const char segmentDelimiter = ':';
public static string Hash(string input)
{
byte[] salt = RandomNumberGenerator.GetBytes(_saltSize);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
input,
salt,
_iterations,
_algorithm,
_keySize
);
return string.Join(
segmentDelimiter,
Convert.ToHexString(hash),
Convert.ToHexString(salt),
_iterations,
_algorithm
);
}
public static bool Verify(string input, string hashString)
{
string[] segments = hashString.Split(segmentDelimiter);
byte[] hash = Convert.FromHexString(segments[0]);
byte[] salt = Convert.FromHexString(segments[1]);
int iterations = int.Parse(segments[2]);
HashAlgorithmName algorithm = new HashAlgorithmName(segments[3]);
byte[] inputHash = Rfc2898DeriveBytes.Pbkdf2(
input,
salt,
iterations,
algorithm,
hash.Length
);
return CryptographicOperations.FixedTimeEquals(inputHash, hash);
}
}
Usage:
// Hash:
string password = "...";
string hashed = SecretHasher.Hash(password);
// Verify:
string enteredPassword = "...";
bool isPasswordCorrect = SecretHasher.Verify(enteredPassword, hashed);
If you dont use asp.net or .net core there is also an easy way in >= .Net Standard 2.0 projects.
First you can set the desired size of the hash, salt and iteration number which is related to the duration of the hash generation:
private const int SaltSize = 32;
private const int HashSize = 32;
private const int IterationCount = 10000;
To generare the password hash and salt you can use something like this:
public static string GeneratePasswordHash(string password, out string salt)
{
using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, SaltSize))
{
rfc2898DeriveBytes.IterationCount = IterationCount;
byte[] hashData = rfc2898DeriveBytes.GetBytes(HashSize);
byte[] saltData = rfc2898DeriveBytes.Salt;
salt = Convert.ToBase64String(saltData);
return Convert.ToBase64String(hashData);
}
}
To verify if the password which the user entered is valid you can check with the values in your database:
public static bool VerifyPassword(string password, string passwordHash, string salt)
{
using (Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, SaltSize))
{
rfc2898DeriveBytes.IterationCount = IterationCount;
rfc2898DeriveBytes.Salt = Convert.FromBase64String(salt);
byte[] hashData = rfc2898DeriveBytes.GetBytes(HashSize);
return Convert.ToBase64String(hashData) == passwordHash;
}
}
The following unit test shows the usage:
string password = "MySecret";
string passwordHash = PasswordHasher.GeneratePasswordHash(password, out string salt);
Assert.True(PasswordHasher.VerifyPassword(password, passwordHash, salt));
Assert.False(PasswordHasher.VerifyPassword(password.ToUpper(), passwordHash, salt));
Microsoft Rfc2898DeriveBytes Source
This is how I do it.. I create the hash and store it using the ProtectedData
api:
public static string GenerateKeyHash(string Password)
{
if (string.IsNullOrEmpty(Password)) return null;
if (Password.Length < 1) return null;
byte[] salt = new byte[20];
byte[] key = new byte[20];
byte[] ret = new byte[40];
try
{
using (RNGCryptoServiceProvider randomBytes = new RNGCryptoServiceProvider())
{
randomBytes.GetBytes(salt);
using (var hashBytes = new Rfc2898DeriveBytes(Password, salt, 10000))
{
key = hashBytes.GetBytes(20);
Buffer.BlockCopy(salt, 0, ret, 0, 20);
Buffer.BlockCopy(key, 0, ret, 20, 20);
}
}
// returns salt/key pair
return Convert.ToBase64String(ret);
}
finally
{
if (salt != null)
Array.Clear(salt, 0, salt.Length);
if (key != null)
Array.Clear(key, 0, key.Length);
if (ret != null)
Array.Clear(ret, 0, ret.Length);
}
}
public static bool ComparePasswords(string PasswordHash, string Password)
{
if (string.IsNullOrEmpty(PasswordHash) || string.IsNullOrEmpty(Password)) return false;
if (PasswordHash.Length < 40 || Password.Length < 1) return false;
byte[] salt = new byte[20];
byte[] key = new byte[20];
byte[] hash = Convert.FromBase64String(PasswordHash);
try
{
Buffer.BlockCopy(hash, 0, salt, 0, 20);
Buffer.BlockCopy(hash, 20, key, 0, 20);
using (var hashBytes = new Rfc2898DeriveBytes(Password, salt, 10000))
{
byte[] newKey = hashBytes.GetBytes(20);
if (newKey != null)
if (newKey.SequenceEqual(key))
return true;
}
return false;
}
finally
{
if (salt != null)
Array.Clear(salt, 0, salt.Length);
if (key != null)
Array.Clear(key, 0, key.Length);
if (hash != null)
Array.Clear(hash, 0, hash.Length);
}
}
public static byte[] DecryptData(string Data, byte[] Salt)
{
if (string.IsNullOrEmpty(Data)) return null;
byte[] btData = Convert.FromBase64String(Data);
try
{
return ProtectedData.Unprotect(btData, Salt, DataProtectionScope.CurrentUser);
}
finally
{
if (btData != null)
Array.Clear(btData, 0, btData.Length);
}
}
public static string EncryptData(byte[] Data, byte[] Salt)
{
if (Data == null) return null;
if (Data.Length < 1) return null;
byte[] buffer = new byte[Data.Length];
try
{
Buffer.BlockCopy(Data, 0, buffer, 0, Data.Length);
return System.Convert.ToBase64String(ProtectedData.Protect(buffer, Salt, DataProtectionScope.CurrentUser));
}
finally
{
if (buffer != null)
Array.Clear(buffer, 0, buffer.Length);
}
}
I read all answers and I think those enough, specially @Michael articles with slow hashing and @CodesInChaos good comments, but I decided to share my code snippet for hashing/validating that may be useful and it does not require [Microsoft.AspNet.Cryptography.KeyDerivation].
private static bool SlowEquals(byte[] a, byte[] b)
{
uint diff = (uint)a.Length ^ (uint)b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
diff |= (uint)(a[i] ^ b[i]);
return diff == 0;
}
private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
{
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
pbkdf2.IterationCount = iterations;
return pbkdf2.GetBytes(outputBytes);
}
private static string CreateHash(string value, int salt_bytes, int hash_bytes, int pbkdf2_iterations)
{
// Generate a random salt
RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
byte[] salt = new byte[salt_bytes];
csprng.GetBytes(salt);
// Hash the value and encode the parameters
byte[] hash = PBKDF2(value, salt, pbkdf2_iterations, hash_bytes);
//You need to return the salt value too for the validation process
return Convert.ToBase64String(hash) + ":" +
Convert.ToBase64String(hash);
}
private static bool ValidateHash(string pureVal, string saltVal, string hashVal, int pbkdf2_iterations)
{
try
{
byte[] salt = Convert.FromBase64String(saltVal);
byte[] hash = Convert.FromBase64String(hashVal);
byte[] testHash = PBKDF2(pureVal, salt, pbkdf2_iterations, hash.Length);
return SlowEquals(hash, testHash);
}
catch (Exception ex)
{
return false;
}
}
Please pay attention SlowEquals function that is so important, Finally, I hope this help and Please don't hesitate to advise me about better approaches.
I use this in .netcore6
public class Encryption
{
public string CreateSalt(int size)
{
//Generate a cryptographic random number.
byte[] buff = new byte[size];
RandomNumberGenerator rng = RandomNumberGenerator.Create();
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}
public string GenerateHash(string input, string salt)
{
byte[] bytes = Encoding.UTF8.GetBytes(input + salt);
SHA256 sha = SHA256.Create();
byte[] hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
public bool Equals(string plainTextInput, string hashedInput, string salt)
{
string newHashedPin = GenerateHash(plainTextInput, salt);
return newHashedPin.Equals(hashedInput);
}
}
There are already good answers to the original question, but I would like to add, that the "SequenceEqual" enables the possibility of a timing attack.
The normal way to check for sequences (of bytes) are the same, is to compare every byte in the order with the second one. The first one that is out of order stops the comparison and "false" is returned.
byte[] hash1 = ...
byte[] hash2 = ...
// can be exploited with a timing attack
bool equals = hash1.SequenceEqual(hash2);
With that, an attacker needs 256 strings with every possible starting byte. He runs every string against the mechanism and the one that takes the longest to get a result is the one with the correct first byte. The attack can then be continued in a similar manner on the next byte... and so on.
I found here a better way of doing it, with a nice explanation.
[MethodImpl(MethodImplOptions.NoOptimization)]
private static bool slowEquals(byte[] a, byte[] b)
{
int diff = a.Length ^ b.Length;
for (int i = 0; i < a.Length && i < b.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
In answer to this part of the original question "Is there any other C# method for hashing passwords" You can achieve this using ASP.NET Identity v3.0 https://www.nuget.org/packages/Microsoft.AspNet.Identity.EntityFramework/3.0.0-rc1-final
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using System.Security.Principal;
namespace HashTest{
class Program
{
static void Main(string[] args)
{
WindowsIdentity wi = WindowsIdentity.GetCurrent();
var ph = new PasswordHasher<WindowsIdentity>();
Console.WriteLine(ph.HashPassword(wi,"test"));
Console.WriteLine(ph.VerifyHashedPassword(wi,"AQAAAAEAACcQAAAAEA5S5X7dmbx/NzTk6ixCX+bi8zbKqBUjBhID3Dg1teh+TRZMkAy3CZC5yIfbLqwk2A==","test"));
}
}
}
protected void m_GenerateSHA256_Button1_Click(objectSender, EventArgs e)
{
string salt =createSalt(10);
string hashedPassword=GenerateSHA256Hash(m_UserInput_TextBox.Text,Salt);
m_SaltHash_TextBox.Text=Salt;
m_SaltSHA256Hash_TextBox.Text=hashedPassword;
}
public string createSalt(int size)
{
var rng= new System.Security.Cyptography.RNGCyptoServiceProvider();
var buff= new byte[size];
rng.GetBytes(buff);
return Convert.ToBase64String(buff);
}
public string GenerateSHA256Hash(string input,string salt)
{
byte[]bytes=System.Text.Encoding.UTF8.GetBytes(input+salt);
new System.Security.Cyptography.SHA256Managed();
byte[]hash=sha256hashString.ComputedHash(bytes);
return bytesArrayToHexString(hash);
}
create proc [dbo].[hash_pass] @family nvarchar(50), @username nvarchar(50), @pass nvarchar(Max),``` @semat nvarchar(50), @tell nvarchar(50)
as insert into tbl_karbar values (@family,@username,(select HASHBYTES('SHA1' ,@pass)),@semat,@tell)
精彩评论