|
| 1 | +using System; |
| 2 | +using System.IO; |
| 3 | +using System.Text; |
| 4 | + |
| 5 | +using Org.BouncyCastle.Utilities; |
| 6 | + |
| 7 | +namespace Org.BouncyCastle.Crypto.Generators |
| 8 | +{ |
| 9 | + /** |
| 10 | + * Password hashing scheme BCrypt, |
| 11 | + * designed by Niels Provos and David Mazières, using the |
| 12 | + * String format and the Base64 encoding |
| 13 | + * of the reference implementation on OpenBSD |
| 14 | + */ |
| 15 | + public class OpenBsdBCrypt |
| 16 | + { |
| 17 | + private static readonly byte[] EncodingTable = // the Bcrypts encoding table for OpenBSD |
| 18 | + { |
| 19 | + (byte)'.', (byte)'/', (byte)'A', (byte)'B', (byte)'C', (byte)'D', |
| 20 | + (byte)'E', (byte)'F', (byte)'G', (byte)'H', (byte)'I', (byte)'J', |
| 21 | + (byte)'K', (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P', |
| 22 | + (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V', |
| 23 | + (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', (byte)'a', (byte)'b', |
| 24 | + (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', (byte)'h', |
| 25 | + (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', |
| 26 | + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', |
| 27 | + (byte)'u', (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', |
| 28 | + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', |
| 29 | + (byte)'6', (byte)'7', (byte)'8', (byte)'9' |
| 30 | + }; |
| 31 | + |
| 32 | + /* |
| 33 | + * set up the decoding table. |
| 34 | + */ |
| 35 | + private static readonly byte[] DecodingTable = new byte[128]; |
| 36 | + private static readonly string Version = "2a"; // previous version was not UTF-8 |
| 37 | + |
| 38 | + static OpenBsdBCrypt() |
| 39 | + { |
| 40 | + for (int i = 0; i < DecodingTable.Length; i++) |
| 41 | + { |
| 42 | + DecodingTable[i] = (byte)0xff; |
| 43 | + } |
| 44 | + |
| 45 | + for (int i = 0; i < EncodingTable.Length; i++) |
| 46 | + { |
| 47 | + DecodingTable[EncodingTable[i]] = (byte)i; |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + public OpenBsdBCrypt() |
| 52 | + { |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Creates a 60 character Bcrypt String, including |
| 57 | + * version, cost factor, salt and hash, separated by '$' |
| 58 | + * |
| 59 | + * @param cost the cost factor, treated as an exponent of 2 |
| 60 | + * @param salt a 16 byte salt |
| 61 | + * @param password the password |
| 62 | + * @return a 60 character Bcrypt String |
| 63 | + */ |
| 64 | + private static string CreateBcryptString(byte[] password, byte[] salt, int cost) |
| 65 | + { |
| 66 | + StringBuilder sb = new StringBuilder(60); |
| 67 | + sb.Append('$'); |
| 68 | + sb.Append(Version); |
| 69 | + sb.Append('$'); |
| 70 | + sb.Append(cost < 10 ? ("0" + cost) : cost.ToString()); |
| 71 | + sb.Append('$'); |
| 72 | + sb.Append(EncodeData(salt)); |
| 73 | + |
| 74 | + byte[] key = BCrypt.Generate(password, salt, cost); |
| 75 | + |
| 76 | + sb.Append(EncodeData(key)); |
| 77 | + |
| 78 | + return sb.ToString(); |
| 79 | + } |
| 80 | + |
| 81 | + /** |
| 82 | + * Creates a 60 character Bcrypt String, including |
| 83 | + * version, cost factor, salt and hash, separated by '$' |
| 84 | + * |
| 85 | + * @param cost the cost factor, treated as an exponent of 2 |
| 86 | + * @param salt a 16 byte salt |
| 87 | + * @param password the password |
| 88 | + * @return a 60 character Bcrypt String |
| 89 | + */ |
| 90 | + public static string Generate(char[] password, byte[] salt, int cost) |
| 91 | + { |
| 92 | + if (password == null) |
| 93 | + throw new ArgumentNullException("password"); |
| 94 | + if (salt == null) |
| 95 | + throw new ArgumentNullException("salt"); |
| 96 | + if (salt.Length != 16) |
| 97 | + throw new DataLengthException("16 byte salt required: " + salt.Length); |
| 98 | + |
| 99 | + if (cost < 4 || cost > 31) // Minimum rounds: 16, maximum 2^31 |
| 100 | + throw new ArgumentException("Invalid cost factor.", "cost"); |
| 101 | + |
| 102 | + byte[] psw = Strings.ToUtf8ByteArray(password); |
| 103 | + |
| 104 | + // 0 termination: |
| 105 | + |
| 106 | + byte[] tmp = new byte[psw.Length >= 72 ? 72 : psw.Length + 1]; |
| 107 | + int copyLen = System.Math.Min(psw.Length, tmp.Length); |
| 108 | + Array.Copy(psw, 0, tmp, 0, copyLen); |
| 109 | + |
| 110 | + Array.Clear(psw, 0, psw.Length); |
| 111 | + |
| 112 | + string rv = CreateBcryptString(tmp, salt, cost); |
| 113 | + |
| 114 | + Array.Clear(tmp, 0, tmp.Length); |
| 115 | + |
| 116 | + return rv; |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Checks if a password corresponds to a 60 character Bcrypt String |
| 121 | + * |
| 122 | + * @param bcryptString a 60 character Bcrypt String, including |
| 123 | + * version, cost factor, salt and hash, |
| 124 | + * separated by '$' |
| 125 | + * @param password the password as an array of chars |
| 126 | + * @return true if the password corresponds to the |
| 127 | + * Bcrypt String, otherwise false |
| 128 | + */ |
| 129 | + public static bool CheckPassword(string bcryptString, char[] password) |
| 130 | + { |
| 131 | + // validate bcryptString: |
| 132 | + if (bcryptString.Length != 60) |
| 133 | + throw new DataLengthException("Bcrypt String length: " + bcryptString.Length + ", 60 required."); |
| 134 | + if (bcryptString[0] != '$' || bcryptString[3] != '$' || bcryptString[6] != '$') |
| 135 | + throw new ArgumentException("Invalid Bcrypt String format.", "bcryptString"); |
| 136 | + if (!bcryptString.Substring(1, 2).Equals(Version)) |
| 137 | + throw new ArgumentException("Wrong Bcrypt version, 2a expected.", "bcryptString"); |
| 138 | + |
| 139 | + int cost = 0; |
| 140 | + try |
| 141 | + { |
| 142 | + cost = Int32.Parse(bcryptString.Substring(4, 2)); |
| 143 | + } |
| 144 | + catch (Exception nfe) |
| 145 | + { |
| 146 | + throw new ArgumentException("Invalid cost factor: " + bcryptString.Substring(4, 2), "bcryptString"); |
| 147 | + } |
| 148 | + if (cost < 4 || cost > 31) |
| 149 | + throw new ArgumentException("Invalid cost factor: " + cost + ", 4 < cost < 31 expected."); |
| 150 | + |
| 151 | + // check password: |
| 152 | + if (password == null) |
| 153 | + throw new ArgumentNullException("Missing password."); |
| 154 | + |
| 155 | + int start = bcryptString.LastIndexOf('$') + 1, end = bcryptString.Length - 31; |
| 156 | + byte[] salt = DecodeSaltString(bcryptString.Substring(start, end - start)); |
| 157 | + |
| 158 | + string newBcryptString = Generate(password, salt, cost); |
| 159 | + |
| 160 | + return bcryptString.Equals(newBcryptString); |
| 161 | + } |
| 162 | + |
| 163 | + /* |
| 164 | + * encode the input data producing a Bcrypt base 64 string. |
| 165 | + * |
| 166 | + * @param a byte representation of the salt or the password |
| 167 | + * @return the Bcrypt base64 string |
| 168 | + */ |
| 169 | + private static string EncodeData(byte[] data) |
| 170 | + { |
| 171 | + if (data.Length != 24 && data.Length != 16) // 192 bit key or 128 bit salt expected |
| 172 | + throw new DataLengthException("Invalid length: " + data.Length + ", 24 for key or 16 for salt expected"); |
| 173 | + |
| 174 | + bool salt = false; |
| 175 | + if (data.Length == 16)//salt |
| 176 | + { |
| 177 | + salt = true; |
| 178 | + byte[] tmp = new byte[18];// zero padding |
| 179 | + Array.Copy(data, 0, tmp, 0, data.Length); |
| 180 | + data = tmp; |
| 181 | + } |
| 182 | + else // key |
| 183 | + { |
| 184 | + data[data.Length - 1] = (byte)0; |
| 185 | + } |
| 186 | + |
| 187 | + MemoryStream mOut = new MemoryStream(); |
| 188 | + int len = data.Length; |
| 189 | + |
| 190 | + uint a1, a2, a3; |
| 191 | + int i; |
| 192 | + for (i = 0; i < len; i += 3) |
| 193 | + { |
| 194 | + a1 = data[i]; |
| 195 | + a2 = data[i + 1]; |
| 196 | + a3 = data[i + 2]; |
| 197 | + |
| 198 | + mOut.WriteByte(EncodingTable[(a1 >> 2) & 0x3f]); |
| 199 | + mOut.WriteByte(EncodingTable[((a1 << 4) | (a2 >> 4)) & 0x3f]); |
| 200 | + mOut.WriteByte(EncodingTable[((a2 << 2) | (a3 >> 6)) & 0x3f]); |
| 201 | + mOut.WriteByte(EncodingTable[a3 & 0x3f]); |
| 202 | + } |
| 203 | + |
| 204 | + string result = Strings.FromByteArray(mOut.ToArray()); |
| 205 | + int resultLen = salt |
| 206 | + ? 22 // truncate padding |
| 207 | + : result.Length - 1; |
| 208 | + |
| 209 | + return result.Substring(0, resultLen); |
| 210 | + } |
| 211 | + |
| 212 | + |
| 213 | + /* |
| 214 | + * decodes the bcrypt base 64 encoded SaltString |
| 215 | + * |
| 216 | + * @param a 22 character Bcrypt base 64 encoded String |
| 217 | + * @return the 16 byte salt |
| 218 | + * @exception DataLengthException if the length |
| 219 | + * of parameter is not 22 |
| 220 | + * @exception InvalidArgumentException if the parameter |
| 221 | + * contains a value other than from Bcrypts base 64 encoding table |
| 222 | + */ |
| 223 | + private static byte[] DecodeSaltString(string saltString) |
| 224 | + { |
| 225 | + char[] saltChars = saltString.ToCharArray(); |
| 226 | + |
| 227 | + MemoryStream mOut = new MemoryStream(16); |
| 228 | + byte b1, b2, b3, b4; |
| 229 | + |
| 230 | + if (saltChars.Length != 22)// bcrypt salt must be 22 (16 bytes) |
| 231 | + throw new DataLengthException("Invalid base64 salt length: " + saltChars.Length + " , 22 required."); |
| 232 | + |
| 233 | + // check string for invalid characters: |
| 234 | + for (int i = 0; i < saltChars.Length; i++) |
| 235 | + { |
| 236 | + int value = saltChars[i]; |
| 237 | + if (value > 122 || value < 46 || (value > 57 && value < 65)) |
| 238 | + throw new ArgumentException("Salt string contains invalid character: " + value, "saltString"); |
| 239 | + } |
| 240 | + |
| 241 | + // Padding: add two '\u0000' |
| 242 | + char[] tmp = new char[22 + 2]; |
| 243 | + Array.Copy(saltChars, 0, tmp, 0, saltChars.Length); |
| 244 | + saltChars = tmp; |
| 245 | + |
| 246 | + int len = saltChars.Length; |
| 247 | + |
| 248 | + for (int i = 0; i < len; i += 4) |
| 249 | + { |
| 250 | + b1 = DecodingTable[saltChars[i]]; |
| 251 | + b2 = DecodingTable[saltChars[i + 1]]; |
| 252 | + b3 = DecodingTable[saltChars[i + 2]]; |
| 253 | + b4 = DecodingTable[saltChars[i + 3]]; |
| 254 | + |
| 255 | + mOut.WriteByte((byte)((b1 << 2) | (b2 >> 4))); |
| 256 | + mOut.WriteByte((byte)((b2 << 4) | (b3 >> 2))); |
| 257 | + mOut.WriteByte((byte)((b3 << 6) | b4)); |
| 258 | + } |
| 259 | + |
| 260 | + byte[] saltBytes = mOut.ToArray(); |
| 261 | + |
| 262 | + // truncate: |
| 263 | + byte[] tmpSalt = new byte[16]; |
| 264 | + Array.Copy(saltBytes, 0, tmpSalt, 0, tmpSalt.Length); |
| 265 | + saltBytes = tmpSalt; |
| 266 | + |
| 267 | + return saltBytes; |
| 268 | + } |
| 269 | + } |
| 270 | +} |
0 commit comments