|
|
@@ -22,53 +22,12 @@ using System.Text;
|
|
|
|
|
|
namespace Renci.SshNet.Security.Cryptography
|
|
|
{
|
|
|
- /// <summary>BCrypt implementation.</summary>
|
|
|
- /// <remarks>
|
|
|
- /// <para>
|
|
|
- /// BCrypt implements OpenBSD-style Blowfish password hashing using the scheme described in
|
|
|
- /// <a href="http://www.usenix.org/event/usenix99/provos/provos_html/index.html">"A Future-
|
|
|
- /// Adaptable Password Scheme"</a> by Niels Provos and David Mazieres.
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// This password hashing system tries to thwart off-line password cracking using a
|
|
|
- /// computationally-intensive hashing algorithm, based on Bruce Schneier's Blowfish cipher.
|
|
|
- /// The work factor of the algorithm is parameterised, so it can be increased as computers
|
|
|
- /// get faster.
|
|
|
- /// </para>
|
|
|
- /// <para>
|
|
|
- /// Usage is really simple. To hash a password for the first time, call the <see
|
|
|
- /// cref="HashPassword(string)"/> method with a random salt, like this:
|
|
|
- /// </para>
|
|
|
- /// <code>string pw_hash = BCrypt.HashPassword(plain_password);</code>
|
|
|
- /// <para>
|
|
|
- /// To check whether a plaintext password matches one that has been hashed previously,
|
|
|
- /// use the <see cref="Verify"/> method:
|
|
|
- /// </para>
|
|
|
- /// <code>
|
|
|
- /// if (BCrypt.Verify(candidate_password, stored_hash))
|
|
|
- /// Console.WriteLine("It matches");
|
|
|
- /// else
|
|
|
- /// Console.WriteLine("It does not match");
|
|
|
- /// </code>
|
|
|
- /// <para>
|
|
|
- /// The <see cref="GenerateSalt()"/> method takes an optional parameter (workFactor) that
|
|
|
- /// determines the computational complexity of the hashing:
|
|
|
- /// </para>
|
|
|
- /// <code>
|
|
|
- /// string strong_salt = BCrypt.GenerateSalt(10);
|
|
|
- /// string stronger_salt = BCrypt.GenerateSalt(12);
|
|
|
- /// </code>
|
|
|
- /// <para>
|
|
|
- /// The amount of work increases exponentially (2^workFactor), so each increment is twice
|
|
|
- /// as much work. The default workFactor is 10, and the valid range is 4 to 31.
|
|
|
- /// </para>
|
|
|
- /// </remarks>
|
|
|
+ /// <summary>
|
|
|
+ /// BCrypt Pbkdf implementation.
|
|
|
+ /// This was originally based on https://github.com/hierynomus/sshj/blob/master/src/main/java/com/hierynomus/sshj/userauth/keyprovider/bcrypt/BCrypt.java .
|
|
|
+ /// </summary>
|
|
|
internal sealed class BCrypt
|
|
|
{
|
|
|
- // BCrypt parameters
|
|
|
- private const int GENSALT_DEFAULT_LOG2_ROUNDS = 10;
|
|
|
- private const int BCRYPT_SALT_LEN = 16;
|
|
|
-
|
|
|
// Blowfish parameters
|
|
|
private const int BLOWFISH_NUM_ROUNDS = 16;
|
|
|
|
|
|
@@ -346,299 +305,10 @@ namespace Renci.SshNet.Security.Cryptography
|
|
|
0x66697368, 0x53776174, 0x44796e61, 0x6d697465,
|
|
|
};
|
|
|
|
|
|
- // bcrypt IV: "OrpheanBeholderScryDoubt"
|
|
|
- private static readonly uint[] _BfCryptCiphertext = {
|
|
|
- 0x4f727068, 0x65616e42, 0x65686f6c,
|
|
|
- 0x64657253, 0x63727944, 0x6f756274
|
|
|
- };
|
|
|
-
|
|
|
- // Table for Base64 encoding
|
|
|
- private static readonly char[] _Base64Code = {
|
|
|
- '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
|
|
|
- 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
|
|
|
- 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
|
|
|
- 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
|
|
- 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
|
|
|
- '6', '7', '8', '9'
|
|
|
- };
|
|
|
-
|
|
|
- // Table for Base64 decoding
|
|
|
- private static readonly int[] _Index64 = {
|
|
|
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
|
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
|
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
|
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
|
- -1, -1, -1, -1, -1, -1, 0, 1, 54, 55,
|
|
|
- 56, 57, 58, 59, 60, 61, 62, 63, -1, -1,
|
|
|
- -1, -1, -1, -1, -1, 2, 3, 4, 5, 6,
|
|
|
- 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
|
- 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
|
|
|
- -1, -1, -1, -1, -1, -1, 28, 29, 30,
|
|
|
- 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
|
|
- 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
|
|
|
- 51, 52, 53, -1, -1, -1, -1, -1
|
|
|
- };
|
|
|
-
|
|
|
// Expanded Blowfish key
|
|
|
private uint[] _P;
|
|
|
private uint[] _S;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Hash a string using the OpenBSD bcrypt scheme and a salt generated by <see
|
|
|
- /// cref="BCrypt.GenerateSalt()"/>.
|
|
|
- /// </summary>
|
|
|
- /// <remarks>Just an alias for HashPassword.</remarks>
|
|
|
- /// <param name="source">The string to hash.</param>
|
|
|
- /// <returns>The hashed string.</returns>
|
|
|
- public static string HashString(string source)
|
|
|
- {
|
|
|
- return HashPassword(source);
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Hash a string using the OpenBSD bcrypt scheme and a salt generated by <see
|
|
|
- /// cref="BCrypt.GenerateSalt()"/>.
|
|
|
- /// </summary>
|
|
|
- /// <remarks>Just an alias for HashPassword.</remarks>
|
|
|
- /// <param name="source"> The string to hash.</param>
|
|
|
- /// <param name="workFactor">The log2 of the number of rounds of hashing to apply - the work
|
|
|
- /// factor therefore increases as 2^workFactor.</param>
|
|
|
- /// <returns>The hashed string.</returns>
|
|
|
- public static string HashString(string source, int workFactor)
|
|
|
- {
|
|
|
- return HashPassword(source, GenerateSalt(workFactor));
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Hash a password using the OpenBSD bcrypt scheme and a salt generated by <see
|
|
|
- /// cref="BCrypt.GenerateSalt()"/>.
|
|
|
- /// </summary>
|
|
|
- /// <param name="input">The password to hash.</param>
|
|
|
- /// <returns>The hashed password.</returns>
|
|
|
- public static string HashPassword(string input)
|
|
|
- {
|
|
|
- return HashPassword(input, GenerateSalt());
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Hash a password using the OpenBSD bcrypt scheme and a salt generated by <see
|
|
|
- /// cref="BCrypt.GenerateSalt(int)"/> using the given <paramref name="workFactor"/>.
|
|
|
- /// </summary>
|
|
|
- /// <param name="input"> The password to hash.</param>
|
|
|
- /// <param name="workFactor">The log2 of the number of rounds of hashing to apply - the work
|
|
|
- /// factor therefore increases as 2^workFactor.</param>
|
|
|
- /// <returns>The hashed password.</returns>
|
|
|
- public static string HashPassword(string input, int workFactor)
|
|
|
- {
|
|
|
- return HashPassword(input, GenerateSalt(workFactor));
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>Hash a password using the OpenBSD bcrypt scheme.</summary>
|
|
|
- /// <exception cref="ArgumentException">Thrown when one or more arguments have unsupported or
|
|
|
- /// illegal values.</exception>
|
|
|
- /// <param name="input">The password to hash.</param>
|
|
|
- /// <param name="salt"> the salt to hash with (perhaps generated using BCrypt.gensalt).</param>
|
|
|
- /// <returns>The hashed password</returns>
|
|
|
- public static string HashPassword(string input, string salt)
|
|
|
- {
|
|
|
- if (input == null)
|
|
|
- throw new ArgumentNullException("input");
|
|
|
-
|
|
|
- if (string.IsNullOrEmpty(salt))
|
|
|
- throw new ArgumentException("Invalid salt", "salt");
|
|
|
-
|
|
|
- // Determinthe starting offset and validate the salt
|
|
|
- int startingOffset;
|
|
|
- char minor = (char)0;
|
|
|
- if (salt[0] != '$' || salt[1] != '2')
|
|
|
- throw new SaltParseException("Invalid salt version");
|
|
|
- if (salt[2] == '$')
|
|
|
- startingOffset = 3;
|
|
|
- else
|
|
|
- {
|
|
|
- minor = salt[2];
|
|
|
- if (minor != 'a' || salt[3] != '$')
|
|
|
- throw new SaltParseException("Invalid salt revision");
|
|
|
- startingOffset = 4;
|
|
|
- }
|
|
|
-
|
|
|
- // Extract number of rounds
|
|
|
- if (salt[startingOffset + 2] > '$')
|
|
|
- throw new SaltParseException("Missing salt rounds");
|
|
|
-
|
|
|
- // Extract details from salt
|
|
|
- int logRounds = Convert.ToInt32(salt.Substring(startingOffset, 2));
|
|
|
- string extractedSalt = salt.Substring(startingOffset + 3, 22);
|
|
|
-
|
|
|
- byte[] inputBytes = Encoding.UTF8.GetBytes((input + (minor >= 'a' ? "\0" : "")));
|
|
|
- byte[] saltBytes = DecodeBase64(extractedSalt, BCRYPT_SALT_LEN);
|
|
|
-
|
|
|
- BCrypt bCrypt = new BCrypt();
|
|
|
- byte[] hashed = bCrypt.CryptRaw(inputBytes, saltBytes, logRounds);
|
|
|
-
|
|
|
- // Generate result string
|
|
|
- StringBuilder result = new StringBuilder();
|
|
|
- result.Append("$2");
|
|
|
- if (minor >= 'a')
|
|
|
- result.Append(minor);
|
|
|
- result.AppendFormat("${0:00}$", logRounds);
|
|
|
- result.Append(EncodeBase64(saltBytes, saltBytes.Length));
|
|
|
- result.Append(EncodeBase64(hashed, (_BfCryptCiphertext.Length * 4) - 1));
|
|
|
- return result.ToString();
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Generate a salt for use with the <see cref="BCrypt.HashPassword(string,string)"/> method.
|
|
|
- /// </summary>
|
|
|
- /// <param name="workFactor">The log2 of the number of rounds of hashing to apply - the work
|
|
|
- /// factor therefore increases as 2**workFactor.</param>
|
|
|
- /// <returns>A base64 encoded salt value.</returns>
|
|
|
- public static string GenerateSalt(int workFactor)
|
|
|
- {
|
|
|
- if (workFactor < 4 || workFactor > 31)
|
|
|
- throw new ArgumentOutOfRangeException("workFactor", "The work factor must be between 4 and 31 (inclusive)");
|
|
|
-
|
|
|
- byte[] rnd = new byte[BCRYPT_SALT_LEN];
|
|
|
-
|
|
|
- RandomNumberGenerator rng = RandomNumberGenerator.Create();
|
|
|
-
|
|
|
- rng.GetBytes(rnd);
|
|
|
-
|
|
|
- StringBuilder rs = new StringBuilder();
|
|
|
- rs.AppendFormat("$2a${0:00}$", workFactor);
|
|
|
- rs.Append(EncodeBase64(rnd, rnd.Length));
|
|
|
- return rs.ToString();
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Generate a salt for use with the <see cref="BCrypt.HashPassword(string,string)"/> method
|
|
|
- /// selecting a reasonable default for the number of hashing rounds to apply.
|
|
|
- /// </summary>
|
|
|
- /// <returns>A base64 encoded salt value.</returns>
|
|
|
- public static string GenerateSalt()
|
|
|
- {
|
|
|
- return GenerateSalt(GENSALT_DEFAULT_LOG2_ROUNDS);
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Verifies that the hash of the given <paramref name="text"/> matches the provided
|
|
|
- /// <paramref name="hash"/>
|
|
|
- /// </summary>
|
|
|
- /// <param name="text">The text to verify.</param>
|
|
|
- /// <param name="hash"> The previously-hashed password.</param>
|
|
|
- /// <returns>true if the passwords match, false otherwise.</returns>
|
|
|
- public static bool Verify(string text, string hash)
|
|
|
- {
|
|
|
- return hash == HashPassword(text, hash);
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Encode a byte array using bcrypt's slightly-modified base64 encoding scheme. Note that this
|
|
|
- /// is *not* compatible with the standard MIME-base64 encoding.
|
|
|
- /// </summary>
|
|
|
- /// <exception cref="ArgumentException">Thrown when one or more arguments have unsupported or
|
|
|
- /// illegal values.</exception>
|
|
|
- /// <param name="byteArray">The byte array to encode.</param>
|
|
|
- /// <param name="length"> The number of bytes to encode.</param>
|
|
|
- /// <returns>Base64-encoded string.</returns>
|
|
|
- private static string EncodeBase64(byte[] byteArray, int length)
|
|
|
- {
|
|
|
- if (length <= 0 || length > byteArray.Length)
|
|
|
- throw new ArgumentException("Invalid length", "length");
|
|
|
-
|
|
|
- int off = 0;
|
|
|
- StringBuilder rs = new StringBuilder();
|
|
|
- while (off < length)
|
|
|
- {
|
|
|
- int c1 = byteArray[off++] & 0xff;
|
|
|
- rs.Append(_Base64Code[(c1 >> 2) & 0x3f]);
|
|
|
- c1 = (c1 & 0x03) << 4;
|
|
|
- if (off >= length)
|
|
|
- {
|
|
|
- rs.Append(_Base64Code[c1 & 0x3f]);
|
|
|
- break;
|
|
|
- }
|
|
|
- int c2 = byteArray[off++] & 0xff;
|
|
|
- c1 |= (c2 >> 4) & 0x0f;
|
|
|
- rs.Append(_Base64Code[c1 & 0x3f]);
|
|
|
- c1 = (c2 & 0x0f) << 2;
|
|
|
- if (off >= length)
|
|
|
- {
|
|
|
- rs.Append(_Base64Code[c1 & 0x3f]);
|
|
|
- break;
|
|
|
- }
|
|
|
- c2 = byteArray[off++] & 0xff;
|
|
|
- c1 |= (c2 >> 6) & 0x03;
|
|
|
- rs.Append(_Base64Code[c1 & 0x3f]);
|
|
|
- rs.Append(_Base64Code[c2 & 0x3f]);
|
|
|
- }
|
|
|
- return rs.ToString();
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Decode a string encoded using bcrypt's base64 scheme to a byte array. Note that this is *not*
|
|
|
- /// compatible with the standard MIME-base64 encoding.
|
|
|
- /// </summary>
|
|
|
- /// <exception cref="ArgumentException">Thrown when one or more arguments have unsupported or
|
|
|
- /// illegal values.</exception>
|
|
|
- /// <param name="encodedstring">The string to decode.</param>
|
|
|
- /// <param name="maximumBytes"> The maximum bytes to decode.</param>
|
|
|
- /// <returns>The decoded byte array.</returns>
|
|
|
- private static byte[] DecodeBase64(string encodedstring, int maximumBytes)
|
|
|
- {
|
|
|
- int position = 0,
|
|
|
- sourceLength = encodedstring.Length,
|
|
|
- outputLength = 0;
|
|
|
-
|
|
|
- if (maximumBytes <= 0)
|
|
|
- throw new ArgumentException("Invalid maximum bytes value", "maximumBytes");
|
|
|
-
|
|
|
- // TODO: update to use a List<byte> - it's only ever 16 bytes, so it's not a big deal
|
|
|
- StringBuilder rs = new StringBuilder();
|
|
|
- while (position < sourceLength - 1 && outputLength < maximumBytes)
|
|
|
- {
|
|
|
- int c1 = Char64(encodedstring[position++]);
|
|
|
- int c2 = Char64(encodedstring[position++]);
|
|
|
- if (c1 == -1 || c2 == -1)
|
|
|
- break;
|
|
|
-
|
|
|
- rs.Append((char)((c1 << 2) | ((c2 & 0x30) >> 4)));
|
|
|
- if (++outputLength >= maximumBytes || position >= sourceLength)
|
|
|
- break;
|
|
|
-
|
|
|
- int c3 = Char64(encodedstring[position++]);
|
|
|
- if (c3 == -1)
|
|
|
- break;
|
|
|
-
|
|
|
- rs.Append((char)(((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2)));
|
|
|
- if (++outputLength >= maximumBytes || position >= sourceLength)
|
|
|
- break;
|
|
|
-
|
|
|
- int c4 = Char64(encodedstring[position++]);
|
|
|
- rs.Append((char)(((c3 & 0x03) << 6) | c4));
|
|
|
-
|
|
|
- ++outputLength;
|
|
|
- }
|
|
|
-
|
|
|
- byte[] ret = new byte[outputLength];
|
|
|
- for (position = 0; position < outputLength; position++)
|
|
|
- ret[position] = (byte)rs[position];
|
|
|
- return ret;
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Look up the 3 bits base64-encoded by the specified character, range-checking against
|
|
|
- /// conversion table.
|
|
|
- /// </summary>
|
|
|
- /// <param name="character">The base64-encoded value.</param>
|
|
|
- /// <returns>The decoded value of x.</returns>
|
|
|
- private static int Char64(char character)
|
|
|
- {
|
|
|
- if (character < 0 || character > _Index64.Length)
|
|
|
- return -1;
|
|
|
- return _Index64[character];
|
|
|
- }
|
|
|
-
|
|
|
/// <summary>Blowfish encipher a single 64-bit block encoded as two 32-bit halves.</summary>
|
|
|
/// <param name="blockArray">An array containing the two 32-bit half blocks.</param>
|
|
|
/// <param name="offset"> The position in the array of the blocks.</param>
|
|
|
@@ -759,59 +429,11 @@ namespace Renci.SshNet.Security.Cryptography
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// <summary>Perform the central hashing step in the bcrypt scheme.</summary>
|
|
|
- /// <exception cref="ArgumentException">Thrown when one or more arguments have unsupported or
|
|
|
- /// illegal values.</exception>
|
|
|
- /// <param name="inputBytes">The input byte array to hash.</param>
|
|
|
- /// <param name="saltBytes"> The salt byte array to hash with.</param>
|
|
|
- /// <param name="logRounds"> The binary logarithm of the number of rounds of hashing to apply.</param>
|
|
|
- /// <returns>A byte array containing the hashed result.</returns>
|
|
|
- private byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int logRounds)
|
|
|
- {
|
|
|
- uint[] cdata = new uint[_BfCryptCiphertext.Length];
|
|
|
- Array.Copy(_BfCryptCiphertext, cdata, _BfCryptCiphertext.Length);
|
|
|
- int clen = cdata.Length;
|
|
|
-
|
|
|
- if (logRounds < 4 || logRounds > 31)
|
|
|
- throw new ArgumentException("Bad number of rounds", "logRounds");
|
|
|
-
|
|
|
- if (saltBytes.Length != BCRYPT_SALT_LEN)
|
|
|
- throw new ArgumentException("Bad salt Length", "saltBytes");
|
|
|
-
|
|
|
- uint rounds = 1u << logRounds;
|
|
|
- Debug.Assert(rounds > 0, "Rounds must be > 0"); // We overflowed rounds at 31 - added safety check
|
|
|
-
|
|
|
- InitializeKey();
|
|
|
- EKSKey(saltBytes, inputBytes);
|
|
|
-
|
|
|
- for (int i = 0; i < rounds; i++)
|
|
|
- {
|
|
|
- Key(inputBytes);
|
|
|
- Key(saltBytes);
|
|
|
- }
|
|
|
-
|
|
|
- for (int i = 0; i < 64; i++)
|
|
|
- {
|
|
|
- for (int j = 0; j < (clen >> 1); j++)
|
|
|
- Encipher(cdata, j << 1);
|
|
|
- }
|
|
|
-
|
|
|
- byte[] ret = new byte[clen * 4];
|
|
|
- for (int i = 0, j = 0; i < clen; i++)
|
|
|
- {
|
|
|
- ret[j++] = (byte)((cdata[i] >> 24) & 0xff);
|
|
|
- ret[j++] = (byte)((cdata[i] >> 16) & 0xff);
|
|
|
- ret[j++] = (byte)((cdata[i] >> 8) & 0xff);
|
|
|
- ret[j++] = (byte)(cdata[i] & 0xff);
|
|
|
- }
|
|
|
- return ret;
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* Compatibility with new OpenBSD function.
|
|
|
* Ported from SSHJ library (https://github.com/hierynomus/sshj)
|
|
|
*/
|
|
|
- public void Hash(byte[] hpass, byte[] hsalt, byte[] output)
|
|
|
+ private void Hash(byte[] hpass, byte[] hsalt, byte[] output)
|
|
|
{
|
|
|
InitializeKey();
|
|
|
EKSKey(hsalt, hpass);
|
|
|
@@ -924,29 +546,5 @@ namespace Renci.SshNet.Security.Cryptography
|
|
|
|
|
|
return result;
|
|
|
}
|
|
|
-
|
|
|
- /// <summary>Exception for signalling parse errors. </summary>
|
|
|
- public class SaltParseException : Exception
|
|
|
- {
|
|
|
- /// <summary>Default constructor. </summary>
|
|
|
- public SaltParseException()
|
|
|
- {
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>Initializes a new instance of <see cref="SaltParseException"/>.</summary>
|
|
|
- /// <param name="message">The message.</param>
|
|
|
- public SaltParseException(string message)
|
|
|
- : base(message)
|
|
|
- {
|
|
|
- }
|
|
|
-
|
|
|
- /// <summary>Initializes a new instance of <see cref="SaltParseException"/>.</summary>
|
|
|
- /// <param name="message"> The message.</param>
|
|
|
- /// <param name="innerException">The inner exception.</param>
|
|
|
- public SaltParseException(string message, Exception innerException)
|
|
|
- : base(message, innerException)
|
|
|
- {
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
}
|