From 06afece93a04c168e0b2a29fc31ed55c5662c815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 16 Nov 2022 02:50:25 +0100 Subject: [PATCH] Fix the user code generation logic --- Directory.Build.targets | 1 + .../Startup.cs | 28 +++++++ .../Worker.cs | 2 +- .../Helpers/OpenIddictHelpers.cs | 84 ++++++++++++++++++- .../OpenIddictServerHandlers.Protection.cs | 23 +++-- 5 files changed, 123 insertions(+), 15 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index a3106524..464a2d9a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -63,6 +63,7 @@ $(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION + $(DefineConstants);SUPPORTS_INTEGER32_RANDOM_NUMBER_GENERATOR_METHODS + /// Creates a new containing characters + /// randomly selected in the specified . + /// + /// The characters allowed to be included in the . + /// The desired length of the . + /// A new containing random data. + /// + /// The implementation resolved from is not valid. + /// + public static string CreateRandomString(ReadOnlySpan charset, int length) + { + var algorithm = CryptoConfig.CreateFromName("OpenIddict RNG Cryptographic Provider") switch + { + RandomNumberGenerator result => result, + null => null, + var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName)) + }; + + try + { + var buffer = new char[length]; + + for (var index = 0; index < buffer.Length; index++) + { + // Pick a character in the specified charset by generating a random index. + buffer[index] = charset[index: algorithm switch + { +#if SUPPORTS_INTEGER32_RANDOM_NUMBER_GENERATOR_METHODS + // If no custom random number generator was registered, use + // the static GetInt32() API on platforms that support it. + null => RandomNumberGenerator.GetInt32(0, charset.Length), +#endif + // Otherwise, create a default implementation if necessary + // and use the local function that achieves the same result. + _ => GetInt32(algorithm ??= RandomNumberGenerator.Create(), 0..charset.Length) + }]; + } + + return new string(buffer); + } + + finally + { + algorithm?.Dispose(); + } + + static int GetInt32(RandomNumberGenerator algorithm, Range range) + { + // Note: the logic used here is directly taken from the official implementation + // of the RandomNumberGenerator.GetInt32() method introduced in .NET Core 3.0. + // + // See https://github.com/dotnet/corefx/pull/31243 for more information. + + var count = (uint) range.End.Value - (uint) range.Start.Value - 1; + if (count is 0) + { + return range.Start.Value; + } + + var mask = count; + mask |= mask >> 1; + mask |= mask >> 2; + mask |= mask >> 4; + mask |= mask >> 8; + mask |= mask >> 16; + + var buffer = new byte[sizeof(uint)]; + uint value; + + do + { + algorithm.GetBytes(buffer); + + value = mask & BitConverter.ToUInt32(buffer, 0); + } + + while (value > count); + + return (int) value + range.Start.Value; + } + } + #if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM /// /// Creates a derived key based on the specified using PBKDF2. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index 339f5845..c8bec667 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -1318,24 +1318,21 @@ public static partial class OpenIddictServerHandlers descriptor.Payload = context.Token; descriptor.Principal = context.Principal; - // Note: unlike other reference tokens, user codes are meant to be used by humans, - // who may have to enter it in a web form. To ensure it remains easy enough to type - // even by users with non-Latin keyboards, user codes generated by OpenIddict are - // only compound of 12 digits, generated using a crypto-secure random number generator. - // In this case, the resulting user code is estimated to have at most ~40 bits of entropy. if (context.TokenType is TokenTypeHints.UserCode) { do { - var array = OpenIddictHelpers.CreateRandomArray(size: 12); - var builder = new StringBuilder(array.Length); + // Note: unlike other reference tokens, user codes are meant to be used by humans, + // who may have to enter it in a web form. To ensure it remains easy enough to type + // even by users with non-Latin keyboards, user codes generated by OpenIddict are + // only compound of 12 digits, generated using a crypto-secure random number generator. + // In this case, the resulting user code is estimated to have at most ~40 bits of entropy. - for (var index = 0; index < array.Length; index += 4) - { - builder.AppendFormat(CultureInfo.InvariantCulture, "{0:D4}", BitConverter.ToUInt32(array, index) % 10000); - } + static string CreateRandomNumericCode(int length) => OpenIddictHelpers.CreateRandomString( + charset: stackalloc[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }, + length: length); - descriptor.ReferenceId = builder.ToString(); + descriptor.ReferenceId = CreateRandomNumericCode(length: 12); } // User codes are relatively short. To help reduce the risks of collisions with @@ -1343,9 +1340,9 @@ public static partial class OpenIddictServerHandlers while (await _tokenManager.FindByReferenceIdAsync(descriptor.ReferenceId) is not null); } - // For other tokens, generate a base64url-encoded 256-bit random identifier. else { + // For other tokens, generate a base64url-encoded 256-bit random identifier. descriptor.ReferenceId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256)); }