Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Nothing yet.

## [1.13.0] - 2024-01-21

### Added

- Added `GetBase64String` methods to `RandomStringGenerator`.
- Implemented cryptographically strong string generation from a list of characters.

### Changed

- Improved performance of `RandomStringGenerator.GetString` method.

## [1.12.2] - 2024-01-21

### Fixed
Expand Down Expand Up @@ -161,7 +172,8 @@ Nothing yet.

- Implemented StringExtensions.

[unreleased]: https://github.com/Logitar/Logitar.NET/compare/v1.12.2...HEAD
[unreleased]: https://github.com/Logitar/Logitar.NET/compare/v1.13.0...HEAD
[1.13.0]: https://github.com/Logitar/Logitar.NET/compare/v1.12.2...v1.13.0
[1.12.2]: https://github.com/Logitar/Logitar.NET/compare/v1.12.1...v1.12.2
[1.12.1]: https://github.com/Logitar/Logitar.NET/compare/v1.12.0...v1.12.1
[1.12.0]: https://github.com/Logitar/Logitar.NET/compare/v1.11.0...v1.12.0
Expand Down
65 changes: 46 additions & 19 deletions src/Logitar.Security/Cryptography/RandomStringGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,60 @@ protected RandomStringGenerator()
}

/// <summary>
/// Creates a string with a cryptographically strong random sequence of characters.
/// Creates a string with a cryptographically strong random sequence of 32 bytes (256 bits) converted to Base64.
/// </summary>
/// <param name="bytes">The generated cryptographically strong random sequence of bytes.</param>
/// <returns>The generated cryptographically strong Base64 string.</returns>
public static string GetBase64String(out byte[] bytes)
{
return GetBase64String(256 / 8, out bytes);
}
/// <summary>
/// Creates a string with a cryptographically strong random sequence of bytes converted to Base64.
/// </summary>
/// <param name="count">The number of bytes of random values to created.</param>
/// <param name="bytes">The generated cryptographically strong random sequence of bytes.</param>
/// <returns>The generated cryptographically strong Base64 string.</returns>
public static string GetBase64String(int count, out byte[] bytes)
{
bytes = RandomNumberGenerator.GetBytes(count);
return Convert.ToBase64String(bytes);
}

/// <summary>
/// Creates a string with a cryptographically strong random sequence of characters. The characters will be selected randomly in the following string:
/// <br />!"#$%&amp;'()*+,-./0123456789:;&lt;=&gt;?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
/// </summary>
/// <param name="count">The number of characters of random values to create. Defaults to 32 characters (256 bits).</param>
/// <returns>A string populated with cryptographically strong random characters.</returns>
public static string GetString(int count = 256 / 8)
{
while (true)
return GetString("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", count);
}
/// <summary>
/// Creates a string with a cryptographically strong random sequence of characters from the specified list of characters.
/// </summary>
/// <param name="characters">The list of characters to pick from.</param>
/// <param name="count">The number of characters of random values to create. Defaults to 32 characters (256 bits).</param>
/// <returns>A string populated with cryptographically strong random characters.</returns>
/// <exception cref="ArgumentException">The list of characters contains more than 256 characters.</exception>
public static string GetString(string characters, int count = 256 / 8)
{
const int MaximumLength = byte.MaxValue + 1;
if (characters.Length > MaximumLength)
{
/* In the ASCII table, there are 94 characters between 33 '!' and 126 '~' (126 - 33 + 1 = 94).
* Since there are a total of 256 possibilities, by dividing per 94 and adding a 10% margin we
* generate just a little more bytes than required, obtaining the factor 3. */
byte[] bytes = RandomNumberGenerator.GetBytes(count * 3);
throw new ArgumentException($"A maximum of {MaximumLength} characters must be provided.", nameof(characters));
}

List<byte> secret = new(capacity: count);
for (int i = 0; i < bytes.Length; i++)
{
byte @byte = bytes[i];
if (@byte >= 33 && @byte <= 126)
{
secret.Add(@byte);
char[] s = new char[count];

if (secret.Count == count)
{
return Encoding.ASCII.GetString(secret.ToArray());
}
}
}
byte[] bytes = RandomNumberGenerator.GetBytes(s.Length);
for (int i = 0; i < bytes.Length; i++)
{
// NOTE(fpion): pick a character from the specified list using the randomly generated bytes and assign it to the generated password.
s[i] = characters[bytes[i] % characters.Length];
}

return new string(s);
}
}
6 changes: 3 additions & 3 deletions src/Logitar.Security/Logitar.Security.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/Logitar/Logitar.NET</RepositoryUrl>
<AssemblyVersion>6.0.3.0</AssemblyVersion>
<AssemblyVersion>6.1.0.0</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<Version>6.0.3</Version>
<Version>6.1.0</Version>
<NeutralLanguage>en-CA</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageReleaseNotes>Fixed DateTime claim creation.</PackageReleaseNotes>
<PackageReleaseNotes>Refactored RandomStringGenerator and added GetBase64String methods.</PackageReleaseNotes>
<PackageTags>logitar net framework security</PackageTags>
<PackageProjectUrl>https://github.com/Logitar/Logitar.NET/tree/dev/src/Logitar.Security</PackageProjectUrl>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
[Trait(Traits.Category, Categories.Unit)]
public class RandomStringGeneratorTests
{
[Theory(DisplayName = "It should generate the correct Base64 string.")]
[InlineData(null)]
[InlineData(36)]
public void It_should_generate_the_correct_Base64_string(int? count = null)
{
string generated = count.HasValue ? RandomStringGenerator.GetBase64String(count.Value, out byte[] bytes) : RandomStringGenerator.GetBase64String(out bytes);
Assert.Equal(count ?? (256 / 8), bytes.Length);
Assert.Equal(Convert.ToBase64String(bytes), generated);
}

[Theory(DisplayName = "It should generate the correct string.")]
[InlineData(null)]
[InlineData(20)]
Expand All @@ -12,4 +22,23 @@ public void It_should_generate_the_correct_string(int? count = null)
Assert.Equal(count ?? (256 / 8), generated.Length);
Assert.True(generated.All(c => c >= 33 || c <= 126));
}

[Theory(DisplayName = "It should generate the correct string from a list of characters.")]
[InlineData("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", null)]
[InlineData("ACDEFGHJKLMNPQRSTUVWXYZ2345679", 7)]
public void It_should_generate_the_correct_string_from_a_list_of_characters(string characters, int? count = null)
{
string generated = count.HasValue ? RandomStringGenerator.GetString(characters, count.Value) : RandomStringGenerator.GetString(characters);
Assert.Equal(count ?? (256 / 8), generated.Length);
Assert.True(generated.All(characters.Contains));
}

[Fact(DisplayName = "It should throw ArgumentException when the list of characters is too long.")]
public void It_should_throw_ArgumentException_when_the_list_of_characters_is_too_long()
{
string characters = RandomStringGenerator.GetString(1000);
var exception = Assert.Throws<ArgumentException>(() => RandomStringGenerator.GetString(characters));
Assert.StartsWith("A maximum of 256 characters must be provided.", exception.Message);
Assert.Equal("characters", exception.ParamName);
}
}