Skip to content

Commit 3b50046

Browse files
authored
WIP: Refactored the RandomStringGenerator. (#179)
* Refactored the RandomStringGenerator. * Implemented GetBase64String methods. * Release 1.13.0
1 parent c05cb0e commit 3b50046

File tree

4 files changed

+91
-23
lines changed

4 files changed

+91
-23
lines changed

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
Nothing yet.
1111

12+
## [1.13.0] - 2024-01-21
13+
14+
### Added
15+
16+
- Added `GetBase64String` methods to `RandomStringGenerator`.
17+
- Implemented cryptographically strong string generation from a list of characters.
18+
19+
### Changed
20+
21+
- Improved performance of `RandomStringGenerator.GetString` method.
22+
1223
## [1.12.2] - 2024-01-21
1324

1425
### Fixed
@@ -161,7 +172,8 @@ Nothing yet.
161172

162173
- Implemented StringExtensions.
163174

164-
[unreleased]: https://github.com/Logitar/Logitar.NET/compare/v1.12.2...HEAD
175+
[unreleased]: https://github.com/Logitar/Logitar.NET/compare/v1.13.0...HEAD
176+
[1.13.0]: https://github.com/Logitar/Logitar.NET/compare/v1.12.2...v1.13.0
165177
[1.12.2]: https://github.com/Logitar/Logitar.NET/compare/v1.12.1...v1.12.2
166178
[1.12.1]: https://github.com/Logitar/Logitar.NET/compare/v1.12.0...v1.12.1
167179
[1.12.0]: https://github.com/Logitar/Logitar.NET/compare/v1.11.0...v1.12.0

src/Logitar.Security/Cryptography/RandomStringGenerator.cs

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,60 @@ protected RandomStringGenerator()
1313
}
1414

1515
/// <summary>
16-
/// Creates a string with a cryptographically strong random sequence of characters.
16+
/// Creates a string with a cryptographically strong random sequence of 32 bytes (256 bits) converted to Base64.
17+
/// </summary>
18+
/// <param name="bytes">The generated cryptographically strong random sequence of bytes.</param>
19+
/// <returns>The generated cryptographically strong Base64 string.</returns>
20+
public static string GetBase64String(out byte[] bytes)
21+
{
22+
return GetBase64String(256 / 8, out bytes);
23+
}
24+
/// <summary>
25+
/// Creates a string with a cryptographically strong random sequence of bytes converted to Base64.
26+
/// </summary>
27+
/// <param name="count">The number of bytes of random values to created.</param>
28+
/// <param name="bytes">The generated cryptographically strong random sequence of bytes.</param>
29+
/// <returns>The generated cryptographically strong Base64 string.</returns>
30+
public static string GetBase64String(int count, out byte[] bytes)
31+
{
32+
bytes = RandomNumberGenerator.GetBytes(count);
33+
return Convert.ToBase64String(bytes);
34+
}
35+
36+
/// <summary>
37+
/// Creates a string with a cryptographically strong random sequence of characters. The characters will be selected randomly in the following string:
38+
/// <br />!"#$%&amp;'()*+,-./0123456789:;&lt;=&gt;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
1739
/// </summary>
1840
/// <param name="count">The number of characters of random values to create. Defaults to 32 characters (256 bits).</param>
1941
/// <returns>A string populated with cryptographically strong random characters.</returns>
2042
public static string GetString(int count = 256 / 8)
2143
{
22-
while (true)
44+
return GetString("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", count);
45+
}
46+
/// <summary>
47+
/// Creates a string with a cryptographically strong random sequence of characters from the specified list of characters.
48+
/// </summary>
49+
/// <param name="characters">The list of characters to pick from.</param>
50+
/// <param name="count">The number of characters of random values to create. Defaults to 32 characters (256 bits).</param>
51+
/// <returns>A string populated with cryptographically strong random characters.</returns>
52+
/// <exception cref="ArgumentException">The list of characters contains more than 256 characters.</exception>
53+
public static string GetString(string characters, int count = 256 / 8)
54+
{
55+
const int MaximumLength = byte.MaxValue + 1;
56+
if (characters.Length > MaximumLength)
2357
{
24-
/* In the ASCII table, there are 94 characters between 33 '!' and 126 '~' (126 - 33 + 1 = 94).
25-
* Since there are a total of 256 possibilities, by dividing per 94 and adding a 10% margin we
26-
* generate just a little more bytes than required, obtaining the factor 3. */
27-
byte[] bytes = RandomNumberGenerator.GetBytes(count * 3);
58+
throw new ArgumentException($"A maximum of {MaximumLength} characters must be provided.", nameof(characters));
59+
}
2860

29-
List<byte> secret = new(capacity: count);
30-
for (int i = 0; i < bytes.Length; i++)
31-
{
32-
byte @byte = bytes[i];
33-
if (@byte >= 33 && @byte <= 126)
34-
{
35-
secret.Add(@byte);
61+
char[] s = new char[count];
3662

37-
if (secret.Count == count)
38-
{
39-
return Encoding.ASCII.GetString(secret.ToArray());
40-
}
41-
}
42-
}
63+
byte[] bytes = RandomNumberGenerator.GetBytes(s.Length);
64+
for (int i = 0; i < bytes.Length; i++)
65+
{
66+
// NOTE(fpion): pick a character from the specified list using the randomly generated bytes and assign it to the generated password.
67+
s[i] = characters[bytes[i] % characters.Length];
4368
}
69+
70+
return new string(s);
4471
}
4572
}

src/Logitar.Security/Logitar.Security.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
<PackageReadmeFile>README.md</PackageReadmeFile>
1616
<RepositoryType>git</RepositoryType>
1717
<RepositoryUrl>https://github.com/Logitar/Logitar.NET</RepositoryUrl>
18-
<AssemblyVersion>6.0.3.0</AssemblyVersion>
18+
<AssemblyVersion>6.1.0.0</AssemblyVersion>
1919
<FileVersion>$(AssemblyVersion)</FileVersion>
2020
<PackageLicenseFile>LICENSE</PackageLicenseFile>
2121
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
22-
<Version>6.0.3</Version>
22+
<Version>6.1.0</Version>
2323
<NeutralLanguage>en-CA</NeutralLanguage>
2424
<GenerateDocumentationFile>True</GenerateDocumentationFile>
25-
<PackageReleaseNotes>Fixed DateTime claim creation.</PackageReleaseNotes>
25+
<PackageReleaseNotes>Refactored RandomStringGenerator and added GetBase64String methods.</PackageReleaseNotes>
2626
<PackageTags>logitar net framework security</PackageTags>
2727
<PackageProjectUrl>https://github.com/Logitar/Logitar.NET/tree/dev/src/Logitar.Security</PackageProjectUrl>
2828
</PropertyGroup>

tests/Logitar.Security.UnitTests/Cryptography/RandomStringGeneratorTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
[Trait(Traits.Category, Categories.Unit)]
44
public class RandomStringGeneratorTests
55
{
6+
[Theory(DisplayName = "It should generate the correct Base64 string.")]
7+
[InlineData(null)]
8+
[InlineData(36)]
9+
public void It_should_generate_the_correct_Base64_string(int? count = null)
10+
{
11+
string generated = count.HasValue ? RandomStringGenerator.GetBase64String(count.Value, out byte[] bytes) : RandomStringGenerator.GetBase64String(out bytes);
12+
Assert.Equal(count ?? (256 / 8), bytes.Length);
13+
Assert.Equal(Convert.ToBase64String(bytes), generated);
14+
}
15+
616
[Theory(DisplayName = "It should generate the correct string.")]
717
[InlineData(null)]
818
[InlineData(20)]
@@ -12,4 +22,23 @@ public void It_should_generate_the_correct_string(int? count = null)
1222
Assert.Equal(count ?? (256 / 8), generated.Length);
1323
Assert.True(generated.All(c => c >= 33 || c <= 126));
1424
}
25+
26+
[Theory(DisplayName = "It should generate the correct string from a list of characters.")]
27+
[InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", null)]
28+
[InlineData("ACDEFGHJKLMNPQRSTUVWXYZ2345679", 7)]
29+
public void It_should_generate_the_correct_string_from_a_list_of_characters(string characters, int? count = null)
30+
{
31+
string generated = count.HasValue ? RandomStringGenerator.GetString(characters, count.Value) : RandomStringGenerator.GetString(characters);
32+
Assert.Equal(count ?? (256 / 8), generated.Length);
33+
Assert.True(generated.All(characters.Contains));
34+
}
35+
36+
[Fact(DisplayName = "It should throw ArgumentException when the list of characters is too long.")]
37+
public void It_should_throw_ArgumentException_when_the_list_of_characters_is_too_long()
38+
{
39+
string characters = RandomStringGenerator.GetString(1000);
40+
var exception = Assert.Throws<ArgumentException>(() => RandomStringGenerator.GetString(characters));
41+
Assert.StartsWith("A maximum of 256 characters must be provided.", exception.Message);
42+
Assert.Equal("characters", exception.ParamName);
43+
}
1544
}

0 commit comments

Comments
 (0)