Skip to content

Commit adc5dc0

Browse files
committed
Improvements and bug fixes after a personal code review, as well as independent professional audit (Cure53 team).
-AesCtrCryptoTransform improvements. -CryptoRandom bug fixes. "NextDouble()" now generates 52 bits of entropy. "NextBytes()" is rewritten to avoid a possible serious concurrency flaw. -Minor improvements and fixes throughout the codebase. -Version bump to 1.4.
1 parent 78fae76 commit adc5dc0

19 files changed

+153
-126
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,5 @@ _Doc/*
238238
/Mac/HMAC2*.cs
239239
/.vs/config/applicationhost.config
240240
/Inferno.1.0.0.nuspec
241+
/COMMITS.md
242+
*.snk

Cipher/AesCtrCryptoTransform.cs

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ internal static class AesConstants
1313

1414
public class AesCtrCryptoTransform : ICryptoTransform
1515
{
16-
byte[] counterBuffer = new byte[AesConstants.AES_BLOCK_SIZE];
16+
byte[] counterBuffer_KeyStreamBuffer = new byte[AesConstants.AES_BLOCK_SIZE * 2];
17+
int keyStreamBytesRemaining;
1718

1819
Aes aes;
1920
readonly ICryptoTransform cryptoTransform;
2021

2122
public bool CanReuseTransform { get { return false; } }
2223
public bool CanTransformMultipleBlocks { get { return true; } }
23-
public int InputBlockSize { get { return AesConstants.AES_BLOCK_SIZE; } }
24-
public int OutputBlockSize { get { return AesConstants.AES_BLOCK_SIZE; } }
24+
public int InputBlockSize { get { return 1; } }
25+
public int OutputBlockSize { get { return 1; } }
2526

2627
/// <summary>ctor</summary>
2728
public AesCtrCryptoTransform(byte[] key, ArraySegment<byte> counterBufferSegment, Func<Aes> aesFactory = null)
@@ -33,39 +34,61 @@ public AesCtrCryptoTransform(byte[] key, ArraySegment<byte> counterBufferSegment
3334
this.aes.Mode = CipherMode.ECB;
3435
this.aes.Padding = PaddingMode.None;
3536

36-
Utils.BlockCopy(counterBufferSegment.Array, counterBufferSegment.Offset, counterBuffer, 0, AesConstants.AES_BLOCK_SIZE);
37+
Utils.BlockCopy(counterBufferSegment.Array, counterBufferSegment.Offset, counterBuffer_KeyStreamBuffer, 0, AesConstants.AES_BLOCK_SIZE);
3738
this.cryptoTransform = aes.CreateEncryptor(rgbKey: key, rgbIV: null);
3839
}// ctor
3940

4041
#region public
4142
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
4243
{
43-
int partialBlockSize = inputCount % AesConstants.AES_BLOCK_SIZE;
44-
int fullBlockSize = inputCount & (-AesConstants.AES_BLOCK_SIZE);//inputCount - partialBlockSize;
45-
int i, j;
46-
byte[] counterBuffer = this.counterBuffer; // looks dumb, but local-access is faster than field-access
44+
if (inputCount == 0) return 0;
4745

48-
for (i = outputOffset, /* reusing inputCount as iMax */ inputCount = outputOffset + fullBlockSize; i < inputCount; i += AesConstants.AES_BLOCK_SIZE)
46+
int i, j, k, remainingInputCount = inputCount;
47+
byte[] counterBuffer_KeyStreamBuffer = this.counterBuffer_KeyStreamBuffer; // looks dumb, but local-access is faster than field-access
48+
49+
// process any available key stream first
50+
if (this.keyStreamBytesRemaining > 0)
4951
{
50-
Utils.BlockCopy(counterBuffer, 0, outputBuffer, i, AesConstants.AES_BLOCK_SIZE);
51-
for (j = AesConstants.AES_BLOCK_SIZE - 1; j >= AesConstants.AES_BLOCK_SIZE - AesConstants.COUNTER_SIZE; --j)
52-
if (++counterBuffer[j] != 0) break;
52+
j = inputCount > this.keyStreamBytesRemaining ? this.keyStreamBytesRemaining : inputCount;
53+
for (i = 0; i < j; ++i)
54+
outputBuffer[outputOffset + i] = (byte)(counterBuffer_KeyStreamBuffer[AesConstants.AES_BLOCK_SIZE * 2 - this.keyStreamBytesRemaining + i] ^ inputBuffer[inputOffset + i]);
55+
56+
inputOffset += j;
57+
outputOffset += j;
58+
this.keyStreamBytesRemaining -= j;
59+
remainingInputCount -= j;
60+
61+
if (remainingInputCount == 0) return inputCount;
5362
}
5463

64+
int partialBlockSize = remainingInputCount % AesConstants.AES_BLOCK_SIZE;
65+
int fullBlockSize = remainingInputCount & (-AesConstants.AES_BLOCK_SIZE); // remainingInputCount - partialBlockSize;
66+
67+
// process full blocks, if any
5568
if (fullBlockSize > 0)
5669
{
70+
for (i = outputOffset, /* reusing k as iMax */ k = outputOffset + fullBlockSize; i < k; i += AesConstants.AES_BLOCK_SIZE)
71+
{
72+
Utils.BlockCopy(counterBuffer_KeyStreamBuffer, 0, outputBuffer, i, AesConstants.AES_BLOCK_SIZE);
73+
for (j = AesConstants.AES_BLOCK_SIZE - 1; j >= AesConstants.AES_BLOCK_SIZE - AesConstants.COUNTER_SIZE; --j) if (++counterBuffer_KeyStreamBuffer[j] != 0) break;
74+
}
75+
5776
fullBlockSize = this.cryptoTransform.TransformBlock(outputBuffer, outputOffset, fullBlockSize, outputBuffer, outputOffset);
58-
//for (i = 0; i < fullBlockSize; ++i) outputBuffer[outputOffset + i] ^= inputBuffer[inputOffset + i];
5977
Utils.Xor(outputBuffer, outputOffset, inputBuffer, inputOffset, fullBlockSize);
6078
}
6179

80+
// process the remaining partial block, if any
6281
if (partialBlockSize > 0)
6382
{
64-
outputOffset += fullBlockSize;
6583
inputOffset += fullBlockSize;
66-
this.cryptoTransform.TransformBlock(counterBuffer, 0, AesConstants.AES_BLOCK_SIZE, counterBuffer, 0);
67-
for (i = 0; i < partialBlockSize; ++i) outputBuffer[outputOffset + i] = (byte)(counterBuffer[i] ^ inputBuffer[inputOffset + i]);
84+
outputOffset += fullBlockSize;
85+
86+
this.cryptoTransform.TransformBlock(counterBuffer_KeyStreamBuffer, 0, AesConstants.AES_BLOCK_SIZE, counterBuffer_KeyStreamBuffer, AesConstants.AES_BLOCK_SIZE);
87+
for (j = AesConstants.AES_BLOCK_SIZE - 1; j >= AesConstants.AES_BLOCK_SIZE - AesConstants.COUNTER_SIZE; --j) if (++counterBuffer_KeyStreamBuffer[j] != 0) break;
88+
for (i = 0; i < partialBlockSize; ++i) outputBuffer[outputOffset + i] = (byte)(counterBuffer_KeyStreamBuffer[AesConstants.AES_BLOCK_SIZE + i] ^ inputBuffer[inputOffset + i]);
89+
this.keyStreamBytesRemaining = AesConstants.AES_BLOCK_SIZE - partialBlockSize;
6890
}
91+
6992
return inputCount;
7093
}// TransformBlock()
7194

@@ -88,7 +111,7 @@ public void Dispose()
88111
}
89112
finally
90113
{
91-
Array.Clear(counterBuffer, 0, AesConstants.AES_BLOCK_SIZE);
114+
Array.Clear(this.counterBuffer_KeyStreamBuffer, 0, AesConstants.AES_BLOCK_SIZE * 2);
92115
this.aes = null;
93116
}
94117
}

CryptoRandom.cs

Lines changed: 50 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public long NextLong()
7474
public long NextLong(long maxValue)
7575
{
7676
if (maxValue < 0)
77-
throw new ArgumentOutOfRangeException("maxValue");
77+
throw new ArgumentOutOfRangeException(nameof(maxValue));
7878

7979
return NextLong(0, maxValue);
8080
}//NextLong()
@@ -96,7 +96,7 @@ public long NextLong(long minValue, long maxValue)
9696
return minValue;
9797

9898
if (minValue > maxValue)
99-
throw new ArgumentOutOfRangeException("minValue");
99+
throw new ArgumentOutOfRangeException(nameof(minValue));
100100

101101
ulong diff = decimal.ToUInt64((decimal)maxValue - minValue);
102102
ulong upperBound = ulong.MaxValue / diff * diff;
@@ -136,7 +136,7 @@ public override int Next()
136136
public override int Next(int maxValue)
137137
{
138138
if (maxValue < 0)
139-
throw new ArgumentOutOfRangeException("maxValue");
139+
throw new ArgumentOutOfRangeException(nameof(maxValue));
140140

141141
return Next(0, maxValue);
142142
}//Next()
@@ -158,7 +158,7 @@ public override int Next(int minValue, int maxValue)
158158
return minValue;
159159

160160
if (minValue > maxValue)
161-
throw new ArgumentOutOfRangeException("minValue");
161+
throw new ArgumentOutOfRangeException(nameof(minValue));
162162

163163
long diff = (long)maxValue - minValue;
164164
long upperBound = uint.MaxValue / diff * diff;
@@ -180,8 +180,8 @@ public override int Next(int minValue, int maxValue)
180180
/// </returns>
181181
public override double NextDouble()
182182
{
183-
const double max = 1.0 + uint.MaxValue;
184-
return GetRandomUInt() / max;
183+
const double max = 1L << 53; // https://en.wikipedia.org/wiki/Double-precision_floating-point_format
184+
return (GetRandomULong() >> 11) / max;
185185
}//NextDouble()
186186

187187
/// <summary>
@@ -220,7 +220,7 @@ public void NextBytes(byte[] buffer, int offset, int count)
220220
var checkedBufferSegment = new ArraySegment<byte>(buffer, offset, count); // bounds-validation happens here
221221
if (count == 0) return;
222222
NextBytesInternal(checkedBufferSegment);
223-
}
223+
}//NextBytes()
224224

225225
void NextBytesInternal(ArraySegment<byte> bufferSegment)
226226
{
@@ -236,68 +236,50 @@ void NextBytesInternal(ArraySegment<byte> bufferSegment)
236236
throw new CryptographicException((int)status);
237237
}
238238

239-
while (true)
239+
lock (_byteCache)
240240
{
241-
int currentByteCachePosition = Interlocked.Add(ref _byteCachePosition, count);
242-
if (currentByteCachePosition <= BYTE_CACHE_SIZE && currentByteCachePosition > 0)
241+
if (_byteCachePosition + count <= BYTE_CACHE_SIZE)
243242
{
244-
Utils.BlockCopy(_byteCache, currentByteCachePosition - count, buffer, 0, count);
243+
Utils.BlockCopy(_byteCache, _byteCachePosition, buffer, offset, count);
244+
_byteCachePosition += count;
245245
return;
246246
}
247247

248-
lock (_byteCache)
248+
status = BCrypt.BCryptGenRandom(_byteCache, BYTE_CACHE_SIZE);
249+
if (status == BCrypt.NTSTATUS.STATUS_SUCCESS)
249250
{
250-
currentByteCachePosition = _byteCachePosition; // atomic read
251-
if (currentByteCachePosition > (BYTE_CACHE_SIZE - count) || currentByteCachePosition <= 0)
252-
{
253-
status = BCrypt.BCryptGenRandom(_byteCache, BYTE_CACHE_SIZE);
254-
if (status == BCrypt.NTSTATUS.STATUS_SUCCESS)
255-
{
256-
_byteCachePosition = count; // atomic write
257-
Utils.BlockCopy(_byteCache, 0, buffer, 0, count);
258-
return;
259-
}
260-
261-
// defensive logic to prevent _byteCachePosition from wrapping into valid range due to BCryptGenRandom failures
262-
if (currentByteCachePosition > BYTE_CACHE_SIZE || currentByteCachePosition < 0) _byteCachePosition = BYTE_CACHE_SIZE;
263-
264-
throw new CryptographicException((int)status);
265-
}// if outside the valid range
266-
}// lock
267-
}// while(true)
268-
}//NextBytes()
251+
_byteCachePosition = count;
252+
Utils.BlockCopy(_byteCache, 0, buffer, offset, count);
253+
return;
254+
}
255+
throw new CryptographicException((int)status);
256+
}// lock
257+
}//NextBytesInternal()
269258

270259
/// <summary>
271260
/// Gets one random unsigned 32bit integer in a thread safe manner.
272261
/// </summary>
273262
uint GetRandomUInt()
274263
{
275264
BCrypt.NTSTATUS status;
276-
while (true)
265+
266+
lock (_byteCache)
277267
{
278-
int currentByteCachePosition = Interlocked.Add(ref _byteCachePosition, sizeof(uint));
279-
if (currentByteCachePosition <= BYTE_CACHE_SIZE && currentByteCachePosition > 0)
280-
return BitConverter.ToUInt32(_byteCache, currentByteCachePosition - sizeof(uint));
268+
if (_byteCachePosition + sizeof(uint) <= BYTE_CACHE_SIZE)
269+
{
270+
var result = BitConverter.ToUInt32(_byteCache, _byteCachePosition);
271+
_byteCachePosition += sizeof(uint);
272+
return result;
273+
}
281274

282-
lock (_byteCache)
275+
status = BCrypt.BCryptGenRandom(_byteCache, BYTE_CACHE_SIZE);
276+
if (status == BCrypt.NTSTATUS.STATUS_SUCCESS)
283277
{
284-
currentByteCachePosition = _byteCachePosition; // atomic read
285-
if (currentByteCachePosition > (BYTE_CACHE_SIZE - sizeof(uint)) || currentByteCachePosition <= 0)
286-
{
287-
status = BCrypt.BCryptGenRandom(_byteCache, BYTE_CACHE_SIZE);
288-
if (status == BCrypt.NTSTATUS.STATUS_SUCCESS)
289-
{
290-
_byteCachePosition = sizeof(uint); // atomic write
291-
return BitConverter.ToUInt32(_byteCache, 0);
292-
}
293-
294-
// defensive logic to prevent _byteCachePosition from wrapping into valid range due to BCryptGenRandom failures
295-
if (currentByteCachePosition > BYTE_CACHE_SIZE || currentByteCachePosition < 0) _byteCachePosition = BYTE_CACHE_SIZE;
296-
297-
throw new CryptographicException((int)status);
298-
}// if outside the valid range
299-
}// lock
300-
}// while(true)
278+
_byteCachePosition = sizeof(uint);
279+
return BitConverter.ToUInt32(_byteCache, 0);
280+
}
281+
throw new CryptographicException((int)status);
282+
}// lock
301283
}//GetRandomUInt()
302284

303285
/// <summary>
@@ -306,31 +288,24 @@ uint GetRandomUInt()
306288
ulong GetRandomULong()
307289
{
308290
BCrypt.NTSTATUS status;
309-
while (true)
291+
292+
lock (_byteCache)
310293
{
311-
int currentByteCachePosition = Interlocked.Add(ref _byteCachePosition, sizeof(ulong));
312-
if (currentByteCachePosition <= BYTE_CACHE_SIZE && currentByteCachePosition > 0)
313-
return BitConverter.ToUInt64(_byteCache, currentByteCachePosition - sizeof(ulong));
294+
if (_byteCachePosition + sizeof(ulong) <= BYTE_CACHE_SIZE)
295+
{
296+
var result = BitConverter.ToUInt64(_byteCache, _byteCachePosition);
297+
_byteCachePosition += sizeof(ulong);
298+
return result;
299+
}
314300

315-
lock (_byteCache)
301+
status = BCrypt.BCryptGenRandom(_byteCache, BYTE_CACHE_SIZE);
302+
if (status == BCrypt.NTSTATUS.STATUS_SUCCESS)
316303
{
317-
currentByteCachePosition = _byteCachePosition; // atomic read
318-
if (currentByteCachePosition > (BYTE_CACHE_SIZE - sizeof(ulong)) || currentByteCachePosition <= 0)
319-
{
320-
status = BCrypt.BCryptGenRandom(_byteCache, BYTE_CACHE_SIZE);
321-
if (status == BCrypt.NTSTATUS.STATUS_SUCCESS)
322-
{
323-
_byteCachePosition = sizeof(ulong); // atomic write
324-
return BitConverter.ToUInt64(_byteCache, 0);
325-
}
326-
327-
// defensive logic to prevent _byteCachePosition from wrapping into valid range due to BCryptGenRandom failures
328-
if (currentByteCachePosition > BYTE_CACHE_SIZE || currentByteCachePosition < 0) _byteCachePosition = BYTE_CACHE_SIZE;
329-
330-
throw new CryptographicException((int)status);
331-
}// if outside the valid range
332-
}// lock
333-
}// while(true)
304+
_byteCachePosition = sizeof(ulong);
305+
return BitConverter.ToUInt64(_byteCache, 0);
306+
}
307+
throw new CryptographicException((int)status);
308+
}// lock
334309
}//GetRandomULong()
335310
}//class CryptoRandom
336311

EtM_CBC.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static void Encrypt(byte[] masterKey, ArraySegment<byte> plaintext, byte[
5252
int finalBlockLength = plaintext.Count % Cipher.AesConstants.AES_BLOCK_SIZE;
5353
int paddingLength = Cipher.AesConstants.AES_BLOCK_SIZE - finalBlockLength;
5454
int ciphertextLength = CONTEXT_BUFFER_LENGTH + plaintext.Count + paddingLength + MAC_LENGTH;
55-
if (output.Length - outputOffset < ciphertextLength) throw new ArgumentOutOfRangeException("output", "'output' array segment is not big enough for the ciphertext");
55+
if (output.Length - outputOffset < ciphertextLength) throw new ArgumentOutOfRangeException(nameof(output), $"'{nameof(output)}' array segment is not big enough for the ciphertext");
5656

5757
try
5858
{

EtM_CTR.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static void ClearKeyMaterial()
3535
public static void Encrypt(byte[] masterKey, ArraySegment<byte> plaintext, byte[] output, int outputOffset, ArraySegment<byte>? salt = null, uint counter = 1)
3636
{
3737
int ciphertextLength = CONTEXT_BUFFER_LENGTH + plaintext.Count + MAC_LENGTH;
38-
if (output.Length - outputOffset < ciphertextLength) throw new ArgumentOutOfRangeException("output", "'output' array segment is not big enough for the ciphertext");
38+
if (output.Length - outputOffset < ciphertextLength) throw new ArgumentOutOfRangeException(nameof(output), $"'{nameof(output)}' array segment is not big enough for the ciphertext");
3939
try
4040
{
4141
_cryptoRandom.NextBytes(_contextBuffer.Value);

EtM_Transforms.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class EtM_EncryptTransform : ICryptoTransform
2828
/// <summary>ctor</summary>
2929
public EtM_EncryptTransform(byte[] key, ArraySegment<byte>? salt = null, uint chunkNumber = 1)
3030
{
31-
if (key == null) throw new ArgumentNullException("key", "key cannot be null.");
31+
if (key == null) throw new ArgumentNullException(nameof(key), nameof(key) + " cannot be null.");
3232
this.key = key;
3333
this.salt = salt;
3434
this.currentChunkNumber = chunkNumber;
@@ -53,7 +53,7 @@ public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, b
5353
output: outputBuffer,
5454
outputOffset: outputOffset + j,
5555
salt: this.salt,
56-
counter: this.currentChunkNumber++);
56+
counter: checked(this.currentChunkNumber++));
5757
}
5858
}
5959
return j;
@@ -104,7 +104,7 @@ public class EtM_DecryptTransform : ICryptoTransform
104104
/// <summary>ctor</summary>
105105
public EtM_DecryptTransform(byte[] key, ArraySegment<byte>? salt = null, uint chunkNumber = 1, bool authenticateOnly = false)
106106
{
107-
if (key == null) throw new ArgumentNullException("key", "key cannot be null.");
107+
if (key == null) throw new ArgumentNullException(nameof(key), nameof(key) + " cannot be null.");
108108
this.key = key;
109109
this.salt = salt;
110110
this.currentChunkNumber = chunkNumber;
@@ -152,7 +152,7 @@ public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, b
152152
this.key = null;
153153
throw new CryptographicException("Decryption failed for block " + this.currentChunkNumber.ToString() + ".");
154154
}
155-
++this.currentChunkNumber;
155+
this.currentChunkNumber = checked(this.currentChunkNumber + 1);
156156
}
157157
}
158158
return j;

Extensions/B64Extensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public static string ToB64(this ArraySegment<byte> inputSegment)
8484
public static byte[] FromB64(this string input)
8585
{
8686
if (input == null)
87-
throw new ArgumentNullException("input");
87+
throw new ArgumentNullException(nameof(input));
8888

8989
int len = input.Length;
9090
if (len < 1)

Extensions/Base16Config.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public Base16Config(char[] alphabet = null)
2121
}
2222

2323
if (alphabet.Length != BASE)
24-
throw new ArgumentOutOfRangeException("alphabet", "'alphabet' array must have exactly " + BASE + " characters.");
24+
throw new ArgumentOutOfRangeException(nameof(alphabet), $"'{nameof(alphabet)}' array must have exactly {BASE.ToString()} characters.");
2525

2626
this.Base16table = alphabet;
2727

0 commit comments

Comments
 (0)