Skip to content

Commit 26c0d3e

Browse files
committed
Port bcrypt from Java API
- Requested in BMA-143
1 parent 651ac04 commit 26c0d3e

File tree

8 files changed

+1225
-7
lines changed

8 files changed

+1225
-7
lines changed

crypto/BouncyCastle.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<PropertyGroup>
44
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -631,6 +631,8 @@
631631
<Compile Include="src\crypto\digests\NonMemoableDigest.cs" />
632632
<Compile Include="src\crypto\engines\SerpentEngineBase.cs" />
633633
<Compile Include="src\crypto\engines\TnepresEngine.cs" />
634+
<Compile Include="src\crypto\generators\BCrypt.cs" />
635+
<Compile Include="src\crypto\generators\OpenBsdBCrypt.cs" />
634636
<Compile Include="src\crypto\IAsymmetricBlockCipher.cs" />
635637
<Compile Include="src\crypto\IAsymmetricCipherKeyPairGenerator.cs" />
636638
<Compile Include="src\crypto\IBasicAgreement.cs" />

crypto/crypto.csproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3718,6 +3718,11 @@
37183718
SubType = "Code"
37193719
BuildAction = "Compile"
37203720
/>
3721+
<File
3722+
RelPath = "src\crypto\generators\BCrypt.cs"
3723+
SubType = "Code"
3724+
BuildAction = "Compile"
3725+
/>
37213726
<File
37223727
RelPath = "src\crypto\generators\DESedeKeyGenerator.cs"
37233728
SubType = "Code"
@@ -3808,6 +3813,11 @@
38083813
SubType = "Code"
38093814
BuildAction = "Compile"
38103815
/>
3816+
<File
3817+
RelPath = "src\crypto\generators\OpenBsdBCrypt.cs"
3818+
SubType = "Code"
3819+
BuildAction = "Compile"
3820+
/>
38113821
<File
38123822
RelPath = "src\crypto\generators\OpenSSLPBEParametersGenerator.cs"
38133823
SubType = "Code"
@@ -11249,6 +11259,11 @@
1124911259
SubType = "Code"
1125011260
BuildAction = "Compile"
1125111261
/>
11262+
<File
11263+
RelPath = "test\src\crypto\test\BCryptTest.cs"
11264+
SubType = "Code"
11265+
BuildAction = "Compile"
11266+
/>
1125211267
<File
1125311268
RelPath = "test\src\crypto\test\BlockCipherVectorTest.cs"
1125411269
SubType = "Code"
@@ -11529,6 +11544,11 @@
1152911544
SubType = "Code"
1153011545
BuildAction = "Compile"
1153111546
/>
11547+
<File
11548+
RelPath = "test\src\crypto\test\OpenBsdBCryptTest.cs"
11549+
SubType = "Code"
11550+
BuildAction = "Compile"
11551+
/>
1153211552
<File
1153311553
RelPath = "test\src\crypto\test\PaddingTest.cs"
1153411554
SubType = "Code"

crypto/src/crypto/generators/BCrypt.cs

Lines changed: 617 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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+
}

crypto/test/UnitTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
<Compile Include="src\crypto\test\AESTest.cs" />
156156
<Compile Include="src\crypto\test\AESWrapTest.cs" />
157157
<Compile Include="src\crypto\test\AllTests.cs" />
158+
<Compile Include="src\crypto\test\BCryptTest.cs" />
158159
<Compile Include="src\crypto\test\BlockCipherMonteCarloTest.cs" />
159160
<Compile Include="src\crypto\test\BlockCipherVectorTest.cs" />
160161
<Compile Include="src\crypto\test\BlowfishTest.cs" />
@@ -212,6 +213,7 @@
212213
<Compile Include="src\crypto\test\NullTest.cs" />
213214
<Compile Include="src\crypto\test\OAEPTest.cs" />
214215
<Compile Include="src\crypto\test\OCBTest.cs" />
216+
<Compile Include="src\crypto\test\OpenBsdBCryptTest.cs" />
215217
<Compile Include="src\crypto\test\PSSBlindTest.cs" />
216218
<Compile Include="src\crypto\test\PSSTest.cs" />
217219
<Compile Include="src\crypto\test\PaddingTest.cs" />

0 commit comments

Comments
 (0)