Skip to content

Use one-shot HMACSHA1.HashData() for Identity TOTP values #36724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 6, 2021
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
13 changes: 12 additions & 1 deletion src/Identity/Extensions.Core/src/AuthenticatorTokenProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,24 @@ public virtual async Task<bool> ValidateAsync(string purpose, string token, User
return false;
}

using var hash = new HMACSHA1(Base32.FromBase32(key));
var keyBytes = Base32.FromBase32(key);

#if NET6_0_OR_GREATER
var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
#else
using var hash = new HMACSHA1(keyBytes);
var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
#endif

var timestep = Convert.ToInt64(unixTimestamp / 30);
// Allow codes from 90s in each direction (we could make this configurable?)
for (int i = -2; i <= 2; i++)
{
#if NET6_0_OR_GREATER
var expectedCode = Rfc6238AuthenticationService.ComputeTotp(keyBytes, (ulong)(timestep + i), modifier: null);
#else
var expectedCode = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)(timestep + i), modifier: null);
#endif
if (expectedCode == code)
{
return true;
Expand Down
28 changes: 26 additions & 2 deletions src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,27 @@ public static byte[] GenerateRandomKey()
return bytes;
}

internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
internal static int ComputeTotp(
#if NET6_0_OR_GREATER
byte[] key,
#else
HashAlgorithm hashAlgorithm,
#endif
ulong timestepNumber,
string modifier)
{
// # of 0's = length of pin
const int Mod = 1000000;

// See https://tools.ietf.org/html/rfc4226
// We can add an optional modifier
var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));

#if NET6_0_OR_GREATER
var hash = HMACSHA1.HashData(key, ApplyModifier(timestepAsBytes, modifier));
#else
var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));
#endif

// Generate DT string
var offset = hash[hash.Length - 1] & 0xf;
Expand All @@ -53,7 +65,7 @@ internal static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumbe

private static byte[] ApplyModifier(byte[] input, string modifier)
{
if (String.IsNullOrEmpty(modifier))
if (string.IsNullOrEmpty(modifier))
{
return input;
}
Expand Down Expand Up @@ -85,10 +97,15 @@ public static int GenerateCode(byte[] securityToken, string modifier = null)

// Allow a variance of no greater than 9 minutes in either direction
var currentTimeStep = GetCurrentTimeStepNumber();

#if NET6_0_OR_GREATER
return ComputeTotp(securityToken, currentTimeStep, modifier);
#else
using (var hashAlgorithm = new HMACSHA1(securityToken))
{
return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
}
#endif
}

public static bool ValidateCode(byte[] securityToken, int code, string modifier = null)
Expand All @@ -100,11 +117,18 @@ public static bool ValidateCode(byte[] securityToken, int code, string modifier

// Allow a variance of no greater than 9 minutes in either direction
var currentTimeStep = GetCurrentTimeStepNumber();

#if !NET6_0_OR_GREATER
using (var hashAlgorithm = new HMACSHA1(securityToken))
#endif
{
for (var i = -2; i <= 2; i++)
{
#if NET6_0_OR_GREATER
var computedTotp = ComputeTotp(securityToken, (ulong)((long)currentTimeStep + i), modifier);
#else
var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
#endif
if (computedTotp == code)
{
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
using Xunit;

namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
{
Expand Down Expand Up @@ -50,10 +45,10 @@ internal async Task<ShowRecoveryCodes> SendValidCodeAsync()

public static string ComputeCode(string key)
{
var hash = new HMACSHA1(Base32.FromBase32(key));
var unixTimestamp = Convert.ToInt64(Math.Round((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds));
var keyBytes = Base32.FromBase32(key);
var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var timestep = Convert.ToInt64(unixTimestamp / 30);
var topt = Rfc6238AuthenticationService.ComputeTotp(hash, (ulong)timestep, modifier: null);
var topt = Rfc6238AuthenticationService.ComputeTotp(keyBytes, (ulong)timestep, modifier: null);
return topt.ToString("D6", CultureInfo.InvariantCulture);
}
}
Expand Down