remove unused bcrypt code (#1626)

This commit is contained in:
mus65
2025-04-05 15:50:00 +02:00
committed by GitHub
parent 02c30c88a9
commit ad174829cc
@@ -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)
{
}
}
}
}