Skip to content

Commit 72e82f4

Browse files
authored
Merge pull request #31 from SecurityCze/modulo_bias
2 parents a917ab4 + 386c814 commit 72e82f4

2 files changed

Lines changed: 100 additions & 17 deletions

File tree

Diceware.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ private static void ApplyWordCasing(string[] words, WordCasingType wordCasing, C
165165
break;
166166
case WordCasingType.Random:
167167
char[] randomized = (from c in words[scan].ToCharArray()
168-
select ((random.GetRandomUInt64() & 1) == 0
168+
select (random.CoinToss()
169169
? char.ToUpper(c)
170170
: char.ToLower(c))
171171
)
@@ -194,7 +194,7 @@ private static void ApplyL33tSpeak(string[] words, L33tSpeakType l33tSpeak, Cryp
194194
for (int scan = 0; scan < words.Length; ++scan)
195195
{
196196
bool mutateWord = l33tSpeak.HasFlag(L33tSpeakType.AllWords)
197-
|| (random.GetRandomUInt64() & 1) == 0;
197+
|| random.CoinToss();
198198

199199
if (mutateWord == false)
200200
{
@@ -237,13 +237,12 @@ private static void ApplySalt(string[] words, SaltType salt, IEnumerable<SaltSou
237237
{
238238
for (int scan = 0; scan < words.Length; ++scan)
239239
{
240-
bool skipWord = (random.GetRandomUInt64() & 1) == 0;
241-
if (skipWord)
240+
if (random.CoinToss()) // skip word
242241
{
243242
continue;
244243
}
245244

246-
int insertAt = random.Range(0, words[scan].Length - 1);
245+
int insertAt = random.AtMost(words[scan].Length);
247246

248247
words[scan] = words[scan].Insert(insertAt, GenerateSalt(sources, random));
249248
}
@@ -283,15 +282,10 @@ private static void ApplySalt(string[] words, SaltType salt, IEnumerable<SaltSou
283282
// invalid case: can't insert between a single word.
284283
throw new ArgumentOutOfRangeException(nameof(words.Length), "Salt cannot be inserted between a single word.");
285284
}
286-
// get a random index *between* the first and last word (e.g.: 2nd to 2nd from last)
287-
int targetIndex = random.Range(1, words.Length - 2);
288-
// choose if the salt should go before or after the word at the selected index
289-
bool before = (random.GetRandomUInt64() & 1) == 0;
290-
291-
words[targetIndex] = before
292-
? $"{singleSalt}{separator}{words[targetIndex]}"
293-
: $"{words[targetIndex]}{separator}{singleSalt}";
285+
// get a random index word after which to place salt
286+
int targetIndex = random.AtMost(words.Length - 2);
294287

288+
words[targetIndex] = $"{words[targetIndex]}{separator}{singleSalt}";
295289
break;
296290
default:
297291
throw new ArgumentOutOfRangeException(nameof(salt));

Extensions.cs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,109 @@
11

2+
using System;
3+
24
using KeePassLib.Cryptography;
35

46
namespace KeePassDiceware
57
{
68
internal static class Extensions
79
{
10+
/// <summary>
11+
/// Generates a pseudorandom value in range [0, <paramref name="maxInclusive"/>] (both ends are inclusive).
12+
/// </summary>
13+
/// <param name="maxInclusive"> maximal value (inclusive) of random number.</param>
14+
/// <returns>
15+
/// Pseudorandom value from [0, <paramref name="maxInclusive"/>] range.
16+
/// </returns>
17+
public static ulong AtMost(this CryptoRandomStream random, ulong maxInclusive)
18+
{
19+
const ulong RANDOM_MAX = ulong.MaxValue; // GetRandomUInt64 max return value
20+
if (maxInclusive == RANDOM_MAX)
21+
{
22+
return random.GetRandomUInt64();
23+
}
24+
25+
ulong modulus = maxInclusive + 1;
26+
ulong ceil = RANDOM_MAX - (RANDOM_MAX % modulus);
27+
28+
ulong generated;
29+
do
30+
{
31+
generated = random.GetRandomUInt64();
32+
} while (generated >= ceil);
33+
34+
return generated % modulus;
35+
}
36+
37+
/// <summary>
38+
/// Generates a pseudorandom value in range [0, <paramref name="maxInclusive"/>] (both ends are inclusive).
39+
/// </summary>
40+
/// <param name="maxInclusive"> maximal value (inclusive) of random number.</param>
41+
/// <returns>
42+
/// Pseudorandom value from [0, <paramref name="maxInclusive"/>] range.
43+
/// </returns>
44+
public static int AtMost(this CryptoRandomStream random, int maxInclusive)
45+
{
46+
if (maxInclusive < 0)
47+
{
48+
throw new ArgumentOutOfRangeException(nameof(maxInclusive), $"Must be positive");
49+
}
50+
51+
ulong val = random.AtMost(Convert.ToUInt64(maxInclusive));
52+
53+
return checked((int)val); // maxInclusive <= int.MaxValue -> wont throw
54+
}
55+
56+
/// <summary>
57+
/// Generates a pseudorandom value in range [<paramref name="minInclusive"/>, <paramref name="maxInclusive"/>] (both ends are inclusive).
58+
/// </summary>
59+
/// <param name="minInclusive"> minimal value (inclusive) of random number.</param>
60+
/// <param name="maxInclusive"> maximal value (inclusive) of random number.</param>
61+
/// <returns>
62+
/// Pseudorandom value from [<paramref name="minInclusive"/>, <paramref name="maxInclusive"/>] range.
63+
/// </returns>
64+
/// <exception cref="ArgumentOutOfRangeException">
65+
/// Thrown when the <paramref name="minInclusive"/> is larger than <paramref name="maxInclusive"/>.
66+
/// </exception>
867
public static int Range(this CryptoRandomStream random, int minInclusive, int maxInclusive)
9-
// #todo: avoid modulo bias
10-
=> (int)(random.GetRandomUInt64() % (ulong)(maxInclusive - minInclusive + 1)) + minInclusive;
68+
{
69+
if (minInclusive > maxInclusive)
70+
{
71+
throw new ArgumentOutOfRangeException(nameof(minInclusive), $"Must be smaller or equal to {nameof(maxInclusive)}");
72+
}
1173

74+
int randomValue = AtMost(random, maxInclusive - minInclusive);
75+
76+
return randomValue + minInclusive;
77+
}
78+
79+
/// <summary>
80+
/// Selects a pseudorandom element from <paramref name="array"/>.
81+
/// </summary>
82+
/// <param name="array"> array from which to select the random element.</param>
83+
/// <returns>
84+
/// A pseudorandom element from <paramref name="array"/>.
85+
/// </returns>
86+
/// <exception cref="ArgumentException">
87+
/// Thrown when <paramref name="array"/> is empty.
88+
/// </exception>
1289
public static T SelectRandom<T>(this T[] array, CryptoRandomStream random)
1390
{
14-
int choice = random.Range(0, array.Length - 1);
91+
if (array.Length <= 0)
92+
{
93+
throw new ArgumentException(nameof(array), $"Unable to select a random member of empty object.");
94+
}
95+
96+
int choice = random.AtMost(array.Length - 1);
97+
1598
return array[choice];
1699
}
17100

18-
public static bool CoinToss(this CryptoRandomStream random) => (random.GetRandomUInt64() & 1) == 0;
101+
/// <summary>
102+
/// Return TRUE with 50% probability. Simulates a perfect coin toss.
103+
/// </summary>
104+
/// <returns>
105+
/// TRUE / FALSE with equal probabilities.
106+
/// </returns>
107+
public static bool CoinToss(this CryptoRandomStream random) => (random.GetRandomBytes(1)[0] & 1) == 0;
19108
}
20109
}

0 commit comments

Comments
 (0)